mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 20:48:12 +01:00
Compare commits
129 Commits
v3.0.0-alp
...
pr/3001
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
723065afa7 | ||
|
|
e67305e412 | ||
|
|
33ed3935a3 | ||
|
|
af2f8987a3 | ||
|
|
f6250f979a | ||
|
|
87ecee6308 | ||
|
|
476408f6e5 | ||
|
|
e016fdea18 | ||
|
|
d9a6218524 | ||
|
|
2ae338e8a5 | ||
|
|
a6f93ca1ce | ||
|
|
63266d366d | ||
|
|
5671618c9a | ||
|
|
60497174a3 | ||
|
|
704abf7917 | ||
|
|
a1de006055 | ||
|
|
4f51d19e2b | ||
|
|
196ffbc989 | ||
|
|
a6f58cba14 | ||
|
|
977fed0122 | ||
|
|
bd2d4848d2 | ||
|
|
e3ce1f7a41 | ||
|
|
08f092fd15 | ||
|
|
3f7df7be9b | ||
|
|
99e531d8df | ||
|
|
a47c5ff466 | ||
|
|
60b7e2d69e | ||
|
|
01fa230eae | ||
|
|
e823022b19 | ||
|
|
25e503bc83 | ||
|
|
629c54261a | ||
|
|
fb4e05c65f | ||
|
|
ccbd89c908 | ||
|
|
145fce1b30 | ||
|
|
52a92c658f | ||
|
|
557dca9a22 | ||
|
|
bd23cf3e9d | ||
|
|
4514171a3f | ||
|
|
d76f9c4fe4 | ||
|
|
62f71f3fbc | ||
|
|
6daa8f1e31 | ||
|
|
5783d0d931 | ||
|
|
58568c642d | ||
|
|
02329c78c2 | ||
|
|
9f241de1b5 | ||
|
|
7f420d9bec | ||
|
|
80b701d270 | ||
|
|
21175c8c59 | ||
|
|
dd708da235 | ||
|
|
f1758ef9d0 | ||
|
|
0d1fccc3c5 | ||
|
|
6446130b5d | ||
|
|
8632891323 | ||
|
|
0e3c63eb54 | ||
|
|
c555c1640e | ||
|
|
fffbd1400b | ||
|
|
105ef5721d | ||
|
|
fa214e659f | ||
|
|
8ba5f8e245 | ||
|
|
aea5436d4a | ||
|
|
6ba4056cb4 | ||
|
|
61f4ae0e80 | ||
|
|
c98c9037ed | ||
|
|
3a24cc0c8e | ||
|
|
8e50d366bd | ||
|
|
7a8c00c374 | ||
|
|
c140f5edeb | ||
|
|
0668a399dc | ||
|
|
99bdbdeec1 | ||
|
|
41397ca8bb | ||
|
|
dbc85b6149 | ||
|
|
108d36fd8a | ||
|
|
607d9a7b4e | ||
|
|
5d5f2a02eb | ||
|
|
23ca5a5862 | ||
|
|
955000285b | ||
|
|
dfda53fa20 | ||
|
|
2cb4bd8a72 | ||
|
|
b53f77b304 | ||
|
|
2e8403c7e4 | ||
|
|
d787cd1a2c | ||
|
|
673c8d9937 | ||
|
|
f1728d6476 | ||
|
|
9d011870dc | ||
|
|
c3dcbcf4f8 | ||
|
|
015308fdfe | ||
|
|
9a3008d031 | ||
|
|
85b45f6275 | ||
|
|
f401b68fa7 | ||
|
|
a5d75b661d | ||
|
|
10f12c995c | ||
|
|
8ea95a3ce4 | ||
|
|
71728d3c3f | ||
|
|
2d07cefacc | ||
|
|
b5519a3342 | ||
|
|
a74273124d | ||
|
|
7f1cf5c71f | ||
|
|
e0e8774da5 | ||
|
|
8b207139b4 | ||
|
|
fb36df5302 | ||
|
|
0a8f49275e | ||
|
|
e8c786885b | ||
|
|
1466a2741d | ||
|
|
b27c0f44f5 | ||
|
|
5f42d5df3a | ||
|
|
ae0f231bcc | ||
|
|
19d76c8b75 | ||
|
|
ca659bd6ed | ||
|
|
bc01136da7 | ||
|
|
637f5d342e | ||
|
|
015ceacb11 | ||
|
|
6a42c70b35 | ||
|
|
d33a83e147 | ||
|
|
09492f79f2 | ||
|
|
a8f9a02833 | ||
|
|
d958fc27c2 | ||
|
|
5b1e710800 | ||
|
|
61b6008732 | ||
|
|
a14a09b6ea | ||
|
|
b6ec519ca9 | ||
|
|
67e546504d | ||
|
|
572e567528 | ||
|
|
d29a6cba0e | ||
|
|
ada04f64a5 | ||
|
|
aefa09c69b | ||
|
|
ae30f9423d | ||
|
|
e64fd290b2 | ||
|
|
9821894532 | ||
|
|
114fcd263a |
3
.github/workflows/module.yml
vendored
3
.github/workflows/module.yml
vendored
@@ -43,9 +43,6 @@ jobs:
|
||||
- name: Prepare
|
||||
run: pnpm run dev:prepare
|
||||
|
||||
- name: Devtools prepare
|
||||
run: pnpm run devtools:prepare
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
|
||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## [3.0.0-beta.2](https://github.com/nuxt/ui/compare/v3.0.0-beta.1...v3.0.0-beta.2) (2025-02-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **OverlayProvider:** missing import ([c555c16](https://github.com/nuxt/ui/commit/c555c1640eb9e9aca643d71aa02e2a659485672e))
|
||||
* **useOverlay:** missing imports ([0e3c63e](https://github.com/nuxt/ui/commit/0e3c63eb543981835c56a3bfebe71c4534d3f973))
|
||||
|
||||
## [3.0.0-beta.1](https://github.com/nuxt/ui/compare/v3.0.0-alpha.13...v3.0.0-beta.1) (2025-02-28)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **module:** remove devtools in favor of compodium (#3380)
|
||||
* **useOverlay:** handle programmatic modals and slideovers (#3279)
|
||||
|
||||
### Features
|
||||
|
||||
* **Form:** add prop to disable state transformation ([#3356](https://github.com/nuxt/ui/issues/3356)) ([015ceac](https://github.com/nuxt/ui/commit/015ceacb11f94c6ae0829e59b8dfdef6c325b67d))
|
||||
* **module:** remove devtools in favor of compodium ([#3380](https://github.com/nuxt/ui/issues/3380)) ([7a8c00c](https://github.com/nuxt/ui/commit/7a8c00c37451f7b9b33630f7a790a12c0fb2caa1))
|
||||
* **Table:** add `select` event ([#2822](https://github.com/nuxt/ui/issues/2822)) ([0668a39](https://github.com/nuxt/ui/commit/0668a399dced48d1976de3820118bf2a29fe116e))
|
||||
* **Tree:** new component ([#3180](https://github.com/nuxt/ui/issues/3180)) ([71728d3](https://github.com/nuxt/ui/commit/71728d3c3fa95255db03448c8b87fb75bc2d805c))
|
||||
* **useOverlay:** handle programmatic modals and slideovers ([#3279](https://github.com/nuxt/ui/issues/3279)) ([108d36f](https://github.com/nuxt/ui/commit/108d36fd8a4c7b325fcf85882f054bb5e784de57))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Avatar:** render on SSR ([67e5465](https://github.com/nuxt/ui/commit/67e546504d2eb807c9b707aacc58761b10eb7d37))
|
||||
* **CommandPalette:** wrong ellipsis color ([ada04f6](https://github.com/nuxt/ui/commit/ada04f64a529feab43f5b54162daffe9d3313435))
|
||||
* **components:** missing `$attrs` bind ([#3152](https://github.com/nuxt/ui/issues/3152)) ([fb36df5](https://github.com/nuxt/ui/commit/fb36df5302e379f58ceb9c3a91348256806c8df5))
|
||||
* **Drawer/Modal/Slideover:** disable close autofocus ([ae30f94](https://github.com/nuxt/ui/commit/ae30f9423dfae1f6e27f00176bb3175beeb888e4)), closes [#3227](https://github.com/nuxt/ui/issues/3227)
|
||||
* **Form:** ensure loading state resets to false after an error ([#3359](https://github.com/nuxt/ui/issues/3359)) ([19d76c8](https://github.com/nuxt/ui/commit/19d76c8b75b053f1c44b691ca46c9d3b234dbce1))
|
||||
* **Link:** improve external links handling in vue ([b53f77b](https://github.com/nuxt/ui/commit/b53f77b304a520e925af9d4a752e0f2933cfb71d))
|
||||
* **Modal/Slideover:** add wrapper around title & description ([bc01136](https://github.com/nuxt/ui/commit/bc01136da7e838004088fffbf2acfd6788f3c5fb))
|
||||
* **Modal/Slideover:** fixed header height ([d33a83e](https://github.com/nuxt/ui/commit/d33a83e147b009ebe1e52b7d677d364d18f24333)), closes [#3333](https://github.com/nuxt/ui/issues/3333)
|
||||
* **Modal:** use `dvh` unit ([aefa09c](https://github.com/nuxt/ui/commit/aefa09c69b8f0dbd38d29883dc79d77fc7025f43))
|
||||
* **module:** use key when merging modules options ([9821894](https://github.com/nuxt/ui/commit/98218945328b947e6c2a75f03b89c27e401dc6d5))
|
||||
* **Tooltip:** bind `$attrs` on trigger ([637f5d3](https://github.com/nuxt/ui/commit/637f5d342ee1f1b1bf7de99020df9ca9f07e197e)), closes [#3339](https://github.com/nuxt/ui/issues/3339) [#2897](https://github.com/nuxt/ui/issues/2897)
|
||||
* **vite:** exclude `@nuxt/ui` from vite pre-optimization ([#3352](https://github.com/nuxt/ui/issues/3352)) ([09492f7](https://github.com/nuxt/ui/commit/09492f79f29c0b5dd7a642f76f55aa3d305c0a69))
|
||||
|
||||
## [3.0.0-alpha.13](https://github.com/nuxt/ui/compare/v3.0.0-alpha.12...v3.0.0-alpha.13) (2025-02-17)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
12
README.md
12
README.md
@@ -1,4 +1,8 @@
|
||||
[](https://ui.nuxt.com)
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/91ceab67-89ce-4ef4-8678-4402a92baca5">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/51526d6d-e5ec-41b4-aa37-242dec1cdb27">
|
||||
<img alt="Nuxt UI" src="https://github.com/user-attachments/assets/51526d6d-e5ec-41b4-aa37-242dec1cdb27">
|
||||
</picture>
|
||||
|
||||
# Nuxt UI
|
||||
|
||||
@@ -47,7 +51,7 @@ export default defineNuxtConfig({
|
||||
2. Import Tailwind CSS and Nuxt UI in your CSS:
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
```
|
||||
|
||||
@@ -94,7 +98,7 @@ app.mount('#app')
|
||||
3. Import Tailwind CSS and Nuxt UI in your CSS:
|
||||
|
||||
```css [assets/main.css]
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
```
|
||||
|
||||
@@ -106,7 +110,7 @@ Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/inst
|
||||
- [nuxt/icon](https://github.com/nuxt/icon)
|
||||
- [nuxt/fonts](https://github.com/nuxt/fonts)
|
||||
- [nuxt-modules/color-mode](https://github.com/nuxt-modules/color-mode)
|
||||
- [unovue/radix-vue](https://github.com/unovue/radix-vue)
|
||||
- [unovue/reka-ui](https://github.com/unovue/reka-ui)
|
||||
- [tailwindlabs/tailwindcss](https://github.com/tailwindlabs/tailwindcss)
|
||||
- [vueuse/vueuse](https://github.com/vueuse/vueuse)
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import { defineBuildConfig } from 'unbuild'
|
||||
|
||||
export default defineBuildConfig({
|
||||
entries: [
|
||||
// Include devtools runtime files
|
||||
{ input: './src/devtools/runtime', builder: 'mkdist', outDir: 'dist/devtools/runtime' },
|
||||
// Vue support
|
||||
'./src/unplugin',
|
||||
'./src/vite'
|
||||
@@ -12,8 +10,7 @@ export default defineBuildConfig({
|
||||
emitCJS: true
|
||||
},
|
||||
replace: {
|
||||
'process.env.DEV': 'false',
|
||||
'process.env.NUXT_UI_DEVTOOLS_LOCAL': 'false'
|
||||
'process.env.DEV': 'false'
|
||||
},
|
||||
hooks: {
|
||||
'mkdist:entry:options'(ctx, entry, options) {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'green',
|
||||
neutral: 'zinc'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,198 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from '../../src/devtools/meta'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
|
||||
// Disable devtools in component renderer iframe
|
||||
// @ts-expect-error - Nuxt Devtools internal value
|
||||
window.__NUXT_DEVTOOLS_DISABLE__ = true
|
||||
|
||||
const component = useState<Component | undefined>('__ui-devtools-component')
|
||||
const state = useState<Record<string, any>>('__ui-devtools-state', () => ({}))
|
||||
|
||||
const { data: components, status, error } = useAsyncData<Array<Component>>('__ui-devtools-components', async () => {
|
||||
const componentMeta = await $fetch<Record<string, Component>>('/api/component-meta')
|
||||
|
||||
if (!component.value || !componentMeta[component.value.slug]) {
|
||||
component.value = componentMeta['button']
|
||||
}
|
||||
|
||||
state.value.props = Object.values(componentMeta).reduce((acc, comp) => {
|
||||
const componentDefaultProps = comp.meta?.props.reduce((acc, prop) => {
|
||||
if (prop.default) acc[prop.name] = prop.default
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
|
||||
acc[comp.slug] = {
|
||||
...comp.defaultVariants, // Default values from the theme template
|
||||
...componentDefaultProps, // Default values from vue props
|
||||
...componentMeta[comp.slug]?.meta?.devtools?.defaultProps // Default values from devtools extended meta
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
|
||||
return Object.values(componentMeta)
|
||||
})
|
||||
|
||||
const componentProps = computed(() => {
|
||||
if (!component.value) return
|
||||
return state.value.props[component.value?.slug]
|
||||
})
|
||||
|
||||
const componentPropsMeta = computed(() => {
|
||||
return component.value?.meta?.props.filter(prop => prop.name !== 'ui').sort((a, b) => a.name.localeCompare(b.name))
|
||||
})
|
||||
|
||||
function updateRenderer() {
|
||||
if (!component.value) return
|
||||
const event: Event & { data?: any } = new Event('nuxt-ui-devtools:update-renderer')
|
||||
event.data = {
|
||||
props: state.value.props?.[component.value.slug], slots: state.value.slots?.[component.value?.slug]
|
||||
}
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
watchDebounced(state, updateRenderer, { deep: true, debounce: 200, maxWait: 500 })
|
||||
onMounted(() => window.addEventListener('nuxt-ui-devtools:component-loaded', onComponentLoaded))
|
||||
onUnmounted(() => window.removeEventListener('nuxt-ui-devtools:component-loaded', onComponentLoaded))
|
||||
|
||||
function onComponentLoaded() {
|
||||
if (!component.value) return
|
||||
updateRenderer()
|
||||
}
|
||||
|
||||
const tabs = computed(() => {
|
||||
if (!component.value) return
|
||||
return [
|
||||
{ label: 'Props', slot: 'props', icon: 'i-lucide-settings', disabled: !component.value.meta?.props?.length }
|
||||
]
|
||||
})
|
||||
|
||||
function openDocs() {
|
||||
if (!component.value) return
|
||||
window.parent.open(`https://ui3.nuxt.dev/components/${component.value.slug}`)
|
||||
}
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const isDark = computed({
|
||||
get() {
|
||||
return colorMode.value === 'dark'
|
||||
},
|
||||
set(value) {
|
||||
colorMode.preference = value ? 'dark' : 'light'
|
||||
|
||||
const event: Event & { isDark?: boolean } = new Event('nuxt-ui-devtools:set-color-mode')
|
||||
event.isDark = value
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<div v-if="status === 'pending' || error || !component || !components?.length">
|
||||
<div v-if="error" class="flex flex-col justify-center items-center h-screen w-screen text-center text-(--ui-error)">
|
||||
<UILogo class="h-8" />
|
||||
<UIcon name="i-lucide-circle-alert" size="20" class="mt-2" />
|
||||
<p>
|
||||
{{ (error.data as any)?.error ?? 'Unexpected error' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="top-0 h-[49px] border-b border-(--ui-border) flex justify-center">
|
||||
<span />
|
||||
|
||||
<UInputMenu
|
||||
v-model="component"
|
||||
variant="none"
|
||||
:items="components"
|
||||
placeholder="Search component..."
|
||||
class="top-0 translate-y-0 w-full mx-2"
|
||||
icon="i-lucide-search"
|
||||
/>
|
||||
|
||||
<div class="absolute top-[49px] bottom-0 inset-x-0 grid xl:grid-cols-8 grid-cols-4 bg-(--ui-bg)">
|
||||
<div class="col-span-1 border-r border-(--ui-border) hidden xl:block overflow-y-auto">
|
||||
<UNavigationMenu
|
||||
:items="components.map((c) => ({ ...c, active: c.slug === component?.slug, onSelect: () => component = c }))"
|
||||
orientation="vertical"
|
||||
:ui="{ link: 'before:rounded-none' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-5 col-span-2 relative">
|
||||
<ComponentPreview :component="component" :props="componentProps" class="h-full" />
|
||||
<div class="flex gap-2 absolute top-1 right-2">
|
||||
<UButton
|
||||
:icon="isDark ? 'i-lucide-moon' : 'i-lucide-sun'"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
@click="isDark = !isDark"
|
||||
/>
|
||||
<UButton
|
||||
v-if="component"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
icon="i-lucide-external-link"
|
||||
@click="openDocs()"
|
||||
>
|
||||
Open docs
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-l border-(--ui-border) flex flex-col col-span-2 overflow-y-auto">
|
||||
<UTabs color="neutral" variant="link" :items="tabs" class="relative" :ui="{ list: 'sticky top-0 bg-(--ui-bg) z-50' }">
|
||||
<template #props>
|
||||
<div v-for="prop in componentPropsMeta" :key="'prop-' + prop.name" class="px-3 py-5 border-b border-(--ui-border)">
|
||||
<ComponentPropInput
|
||||
v-model="componentProps[prop.name]"
|
||||
:meta="prop"
|
||||
:ignore="component.meta?.devtools?.ignoreProps?.includes(prop.name)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UTabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import 'tailwindcss';
|
||||
@import '@nuxt/ui';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'DM Sans', sans-serif;
|
||||
|
||||
--color-green-50: #EFFDF5;
|
||||
--color-green-100: #D9FBE8;
|
||||
--color-green-200: #B3F5D1;
|
||||
--color-green-300: #75EDAE;
|
||||
--color-green-400: #00DC82;
|
||||
--color-green-500: #00C16A;
|
||||
--color-green-600: #00A155;
|
||||
--color-green-700: #007F45;
|
||||
--color-green-800: #016538;
|
||||
--color-green-900: #0A5331;
|
||||
--color-green-950: #052E16;
|
||||
}
|
||||
|
||||
.shiki
|
||||
.shiki span {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
html.dark .shiki,
|
||||
html.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: transparent !important;
|
||||
/* Optional, if you also want font styles */
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
const collapsed = ref(true)
|
||||
const wrapper = ref<HTMLElement | null>(null)
|
||||
const content = ref<HTMLElement | null>(null)
|
||||
|
||||
const overflow = computed(() => {
|
||||
if (!content.value || !wrapper.value) return false
|
||||
return content.value.scrollHeight > 48 * 4
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (wrapper.value) {
|
||||
wrapper.value.style.transition = 'max-height 0.3s ease' // Set transition for max-height
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border rounded-(--ui-radius) border-(--ui-border)">
|
||||
<div
|
||||
ref="wrapper"
|
||||
:class="['overflow-hidden', collapsed && overflow ? 'max-h-48' : 'max-h-none']"
|
||||
>
|
||||
<div ref="content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<UButton
|
||||
v-if="overflow"
|
||||
class="bg-(--ui-bg) group w-full flex justify-center my-1 border-t border-(--ui-border) rounded-t-none"
|
||||
variant="link"
|
||||
color="neutral"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
:data-state="collapsed ? 'closed' : 'open'"
|
||||
:ui="{ trailingIcon: 'transition group-data-[state=open]:rotate-180' }"
|
||||
@click="collapsed = !collapsed"
|
||||
>
|
||||
{{ collapsed ? 'Expand' : 'Collapse' }}
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,151 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from '../../../src/devtools/meta'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { kebabCase } from 'scule'
|
||||
import { escapeString } from 'knitwork'
|
||||
|
||||
const props = defineProps<{ component?: Component, props?: object, themeSlots?: Record<string, any> }>()
|
||||
|
||||
const { data: componentExample } = useAsyncData('__ui_devtools_component-source', async () => {
|
||||
const example = props.component?.meta?.devtools?.example
|
||||
if (!example) return false
|
||||
return await $fetch<{ source: string }>(`/api/component-example`, { params: { component: example } })
|
||||
}, { watch: [() => props.component?.slug] })
|
||||
|
||||
function genPropValue(value: any): string {
|
||||
if (typeof value === 'string') {
|
||||
return `'${escapeString(value).replace(/'/g, ''').replace(/"/g, '"')}'`
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[ ${value.map(item => `${genPropValue(item)}`).join(',')} ]`
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const entries = Object.entries(value).map(([key, val]) => `${key}: ${genPropValue(val)}`)
|
||||
return `{ ${entries.join(`,`)} }`
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const code = computed(() => {
|
||||
if (!props.component) return
|
||||
|
||||
const propsTemplate = Object.entries(props.props ?? {})?.map(([key, value]: [string, any]) => {
|
||||
const defaultValue: any = props.component?.meta?.props.find(prop => prop.name === key)?.default
|
||||
if (defaultValue === value) return
|
||||
if (value === true) return kebabCase(key)
|
||||
if (value === false && defaultValue === true) return `:${kebabCase(key)}="false"`
|
||||
if (!value) return
|
||||
if (props.component?.defaultVariants?.[key] === value) return
|
||||
if (typeof value === 'string') return `${kebabCase(key)}=${genPropValue(value)}`
|
||||
return `:${kebabCase(key)}="${genPropValue(value)}"`
|
||||
}).filter(Boolean).join('\n')
|
||||
|
||||
const slotsTemplate = props.themeSlots
|
||||
? genPropValue(Object.keys(props.themeSlots).filter(key => key !== 'base').reduce((acc, key) => {
|
||||
acc[key] = genPropValue(props.themeSlots?.[key])
|
||||
return acc
|
||||
}, {} as Record<string, string>))
|
||||
: undefined
|
||||
|
||||
const extraTemplate = [
|
||||
propsTemplate,
|
||||
props.themeSlots?.base ? `class="${genPropValue(props.themeSlots.base)}"` : null,
|
||||
slotsTemplate && slotsTemplate !== '{}' ? `:ui="${slotsTemplate}"` : null
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
if (componentExample.value) {
|
||||
const componentRegexp = new RegExp(`<${props.component.label}(\\s|\\r|>)`)
|
||||
|
||||
return componentExample.value?.source
|
||||
.replace(/import .* from ['"]#.*['"];?\n+/, '')
|
||||
.replace(componentRegexp, `<${props.component.label} ${extraTemplate}$1`)
|
||||
.replace('v-bind="$attrs"', '')
|
||||
}
|
||||
return `<${props.component.label} ${extraTemplate} />`
|
||||
})
|
||||
|
||||
const { $prettier } = useNuxtApp()
|
||||
const { data: formattedCode } = useAsyncData('__ui-devtools-component-formatted-code', async () => {
|
||||
if (!code.value) return
|
||||
return await $prettier.format(code.value, {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
printWidth: 80
|
||||
})
|
||||
}, { watch: [code] })
|
||||
|
||||
const { codeToHtml } = useShiki()
|
||||
const { data: highlightedCode } = useAsyncData('__ui-devtools-component-highlighted-code', async () => {
|
||||
return formattedCode.value
|
||||
? codeToHtml(formattedCode.value, 'vue')
|
||||
: undefined
|
||||
}, { watch: [formattedCode] })
|
||||
|
||||
const { copy, copied } = useClipboard()
|
||||
|
||||
const rendererVisible = ref(true)
|
||||
const renderer = ref()
|
||||
const rendererReady = ref(false)
|
||||
function onRendererReady() {
|
||||
rendererReady.value = true
|
||||
setTimeout(() => rendererVisible.value = !!renderer.value.contentWindow.document.getElementById('ui-devtools-renderer'), 500)
|
||||
}
|
||||
|
||||
watch(() => props.component, () => rendererReady.value = false)
|
||||
|
||||
const previewUrl = computed(() => {
|
||||
if (!props.component) return
|
||||
const baseUrl = `/__nuxt_ui__/components/${props.component.slug}`
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (props.component?.meta?.devtools?.example !== undefined) {
|
||||
params.append('example', props.component.meta.devtools.example)
|
||||
}
|
||||
const queryString = params.toString()
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col bg-grid">
|
||||
<iframe
|
||||
v-if="component"
|
||||
v-show="rendererReady && rendererVisible"
|
||||
ref="renderer"
|
||||
class="grow w-full"
|
||||
:src="previewUrl"
|
||||
@load="onRendererReady"
|
||||
/>
|
||||
<div v-if="!rendererVisible" class="grow w-full flex justify-center items-center px-8">
|
||||
<UAlert color="error" variant="subtle" title="Component preview not found" icon="i-lucide-circle-alert">
|
||||
<template #description>
|
||||
<p>Ensure your <code>app.vue</code> file includes a <code><NuxtPage /></code> component, as the component preview is mounted as a page. </p>
|
||||
</template>
|
||||
</UAlert>
|
||||
</div>
|
||||
<div v-if="highlightedCode && formattedCode" v-show="rendererReady" class="relative w-full p-3">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<pre class="p-4 min-h-40 max-h-72 text-sm overflow-y-auto rounded-[calc(var(--ui-radius)*1.5)] border border-(--ui-border) bg-(--ui-bg-muted)" v-html="highlightedCode" />
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="link"
|
||||
:icon="copied ? 'i-lucide-clipboard-check' : 'i-lucide-clipboard'"
|
||||
class="absolute top-6 right-6"
|
||||
@click="copy(formattedCode)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bg-grid {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' transform='scale(3)'%3E%3Crect width='100%25' height='100%25' fill='%23fff'/%3E%3Cpath fill='none' stroke='hsla(0, 0%25, 98%25, 1)' stroke-width='.2' d='M10 0v20ZM0 10h20Z'/%3E%3C/svg%3E");
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
|
||||
.dark .bg-grid {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' transform='scale(3)'%3E%3Crect width='100%25' height='100%25' fill='hsl(0, 0%25, 8.5%25)'/%3E%3Cpath fill='none' stroke='hsl(0, 0%25, 11.0%25)' stroke-width='.2' d='M10 0v20ZM0 10h20Z'/%3E%3C/svg%3E");
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,39 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropertyMeta } from 'vue-component-meta'
|
||||
|
||||
const props = defineProps<{ meta: Partial<PropertyMeta>, ignore?: boolean }>()
|
||||
const modelValue = defineModel<any>()
|
||||
|
||||
const matchedInput = shallowRef()
|
||||
const parsedSchema = shallowRef()
|
||||
|
||||
const { resolveInputSchema } = usePropSchema()
|
||||
|
||||
watchEffect(() => {
|
||||
if (!props.meta?.schema) return
|
||||
const result = resolveInputSchema(props.meta.schema)
|
||||
parsedSchema.value = result?.schema
|
||||
matchedInput.value = result?.input
|
||||
})
|
||||
|
||||
const description = computed(() => {
|
||||
return props.meta.description?.replace(/`([^`]+)`/g, '<code class="font-medium bg-(--ui-bg-elevated) px-1 py-0.5 rounded-(--ui-radius)">$1</code>')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UFormField :name="meta?.name" class="" :ui="{ wrapper: 'mb-2' }" :class="{ 'opacity-70 cursor-not-allowed': !matchedInput || ignore }">
|
||||
<template #label>
|
||||
<p v-if="meta?.name" class="font-mono font-bold px-1.5 py-0.5 border border-(--ui-border-accented) border-dashed rounded-(--ui-radius) bg-(--ui-bg-elevated)">
|
||||
{{ meta?.name }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<p v-if="meta.description" class="mt-1" v-html="description" />
|
||||
</template>
|
||||
|
||||
<component :is="matchedInput.component" v-if="!ignore && matchedInput" v-model="modelValue" :schema="parsedSchema" />
|
||||
</UFormField>
|
||||
</template>
|
||||
@@ -1,10 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
width="1020"
|
||||
height="200"
|
||||
viewBox="0 0 1020 200"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-auto h-6 shrink-0 text-(--ui-text)"
|
||||
><path d="M377 200C379.16 200 381 198.209 381 196V103C381 103 386 112 395 127L434 194C435.785 197.74 439.744 200 443 200H470V50H443C441.202 50 439 51.4941 439 54V148L421 116L385 55C383.248 51.8912 379.479 50 376 50H350V200H377Z" fill="currentColor" /><path d="M726 92H739C742.314 92 745 89.3137 745 86V60H773V92H800V116H773V159C773 169.5 778.057 174 787 174H800V200H783C759.948 200 745 185.071 745 160V116H726V92Z" fill="currentColor" /><path d="M591 92V154C591 168.004 585.742 179.809 578 188C570.258 196.191 559.566 200 545 200C530.434 200 518.742 196.191 511 188C503.389 179.809 498 168.004 498 154V92H514C517.412 92 520.769 92.622 523 95C525.231 97.2459 526 98.5652 526 102V154C526 162.059 526.457 167.037 530 171C533.543 174.831 537.914 176 545 176C552.217 176 555.457 174.831 559 171C562.543 167.037 563 162.059 563 154V102C563 98.5652 563.769 96.378 566 94C567.96 91.9107 570.028 91.9599 573 92C573.411 92.0055 574.586 92 575 92H591Z" fill="currentColor" /><path d="M676 144L710 92H684C680.723 92 677.812 93.1758 676 96L660 120L645 97C643.188 94.1758 639.277 92 636 92H611L645 143L608 200H634C637.25 200 640.182 196.787 642 194L660 167L679 195C680.818 197.787 683.75 200 687 200H713L676 144Z" fill="currentColor" /><path d="M168 200H279C282.542 200 285.932 198.756 289 197C292.068 195.244 295.23 193.041 297 190C298.77 186.959 300.002 183.51 300 179.999C299.998 176.488 298.773 173.04 297 170.001L222 41C220.23 37.96 218.067 35.7552 215 34C211.933 32.2448 207.542 31 204 31C200.458 31 197.067 32.2448 194 34C190.933 35.7552 188.77 37.96 187 41L168 74L130 9.99764C128.228 6.95784 126.068 3.75491 123 2C119.932 0.245087 116.542 0 113 0C109.458 0 106.068 0.245087 103 2C99.9323 3.75491 96.7717 6.95784 95 9.99764L2 170.001C0.226979 173.04 0.00154312 176.488 1.90993e-06 179.999C-0.0015393 183.51 0.229648 186.959 2 190C3.77035 193.04 6.93245 195.244 10 197C13.0675 198.756 16.4578 200 20 200H90C117.737 200 137.925 187.558 152 164L186 105L204 74L259 168H186L168 200ZM89 168H40L113 42L150 105L125.491 147.725C116.144 163.01 105.488 168 89 168Z" fill="var(--ui-primary)" /><path d="M958 60.0001H938C933.524 60.0001 929.926 59.9395 927 63C924.074 65.8905 925 67.5792 925 72V141C925 151.372 923.648 156.899 919 162C914.352 166.931 908.468 169 899 169C889.705 169 882.648 166.931 878 162C873.352 156.899 873 151.372 873 141V72.0001C873 67.5793 872.926 65.8906 870 63.0001C867.074 59.9396 863.476 60.0001 859 60.0001H840V141C840 159.023 845.016 173.458 855 184C865.156 194.542 879.893 200 899 200C918.107 200 932.844 194.542 943 184C953.156 173.458 958 159.023 958 141V60.0001Z" fill="var(--ui-primary)" /><path fill-rule="evenodd" clip-rule="evenodd" d="M1000 60.0233L1020 60V77L1020 128V156.007L1020 181L1020 189.004C1020 192.938 1019.98 194.429 1017 197.001C1014.02 199.725 1009.56 200 1005 200H986.001V181.006L986 130.012V70.0215C986 66.1576 986.016 64.5494 989 62.023C991.819 59.6358 995.437 60.0233 1000 60.0233Z" fill="var(--ui-primary)" /></svg>
|
||||
</template>
|
||||
@@ -1,76 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as z from 'zod'
|
||||
|
||||
export const arrayInputSchema = z.object({
|
||||
kind: z.literal('array'),
|
||||
schema: z.array(z.any({}))
|
||||
.or(z.record(z.number(), z.any({})).transform(t => Object.values(t)))
|
||||
.transform((t) => {
|
||||
return t.filter(s => s !== 'undefined')
|
||||
}).pipe(z.array(z.any()).max(1))
|
||||
})
|
||||
|
||||
export type ArrayInputSchema = z.infer<typeof arrayInputSchema>
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
schema: ArrayInputSchema
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<Array<any>>({})
|
||||
|
||||
const itemSchema = computed(() => {
|
||||
return props.schema?.schema[0]
|
||||
})
|
||||
|
||||
function removeArrayItem(index: number) {
|
||||
if (!modelValue.value) return
|
||||
modelValue.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function addArrayItem() {
|
||||
if (!modelValue.value) {
|
||||
modelValue.value = [{}]
|
||||
} else {
|
||||
modelValue.value.push({})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="value, index in modelValue" :key="index" class="relative">
|
||||
<ComponentPropInput
|
||||
:model-value="value"
|
||||
:meta="{ schema: itemSchema }"
|
||||
/>
|
||||
|
||||
<UPopover>
|
||||
<UButton variant="ghost" color="neutral" icon="i-lucide-ellipsis-vertical" class="absolute top-4 right-1" />
|
||||
<template #content>
|
||||
<UButton
|
||||
variant="ghost"
|
||||
color="error"
|
||||
icon="i-lucide-trash"
|
||||
block
|
||||
@click="removeArrayItem(index)"
|
||||
>
|
||||
Remove
|
||||
</UButton>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
icon="i-lucide-plus"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
block
|
||||
class="justify-center mt-4"
|
||||
@click="addArrayItem()"
|
||||
>
|
||||
Add value
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as z from 'zod'
|
||||
|
||||
export const booleanInputSchema = z.literal('boolean').or(z.object({
|
||||
kind: z.literal('enum'),
|
||||
type: z.string().refine((type) => {
|
||||
return type.split('|').some(t => t.trim() === 'boolean')
|
||||
})
|
||||
}))
|
||||
|
||||
export type BooleanInputSchema = z.infer<typeof booleanInputSchema>
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ schema: BooleanInputSchema }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USwitch />
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as z from 'zod'
|
||||
|
||||
export const numberInputSchema = z.literal('number')
|
||||
export type NumberInputSchema = z.infer<typeof numberInputSchema>
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ schema: NumberInputSchema }>()
|
||||
const modelValue = defineModel<number>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInput v-model.number="modelValue" type="number" />
|
||||
</template>
|
||||
@@ -1,38 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as z from 'zod'
|
||||
|
||||
export const objectInputSchema = z.object({
|
||||
kind: z.literal('object'),
|
||||
schema: z.record(z.string(), z.any())
|
||||
})
|
||||
|
||||
export type ObjectInputSchema = z.infer<typeof objectInputSchema>
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
schema: ObjectInputSchema
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<Record<string, any>>({})
|
||||
|
||||
const attributesSchemas = computed(() => {
|
||||
return Object.values(props.schema.schema)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapseContainer>
|
||||
<ComponentPropInput
|
||||
v-for="attributeSchema in attributesSchemas"
|
||||
:key="attributeSchema.name"
|
||||
class="border-b last:border-b-0 border-(--ui-border) p-4"
|
||||
:model-value="modelValue?.[attributeSchema.name]"
|
||||
:meta="attributeSchema"
|
||||
@update:model-value="(value: any) => {
|
||||
if (!modelValue) modelValue ||= {}
|
||||
else modelValue[attributeSchema.name] = value
|
||||
}"
|
||||
/>
|
||||
</CollapseContainer>
|
||||
</template>
|
||||
@@ -1,60 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as z from 'zod'
|
||||
|
||||
export const stringEnumInputSchema = z.object({
|
||||
kind: z.literal('enum'),
|
||||
schema: z.array(z.string())
|
||||
.or(z.record(z.any(), z.string()).transform<string[]>(t => Object.values(t)))
|
||||
.transform<string[]>(t => t.filter(s => s.trim().match(/^["'`]/)).map(s => s.trim().replaceAll(/["'`]/g, '')))
|
||||
.pipe(z.array(z.string()).min(1))
|
||||
})
|
||||
|
||||
export type StringEnumInputSchema = z.infer<typeof stringEnumInputSchema>
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
schema: StringEnumInputSchema
|
||||
}>()
|
||||
|
||||
const sizes = ['xs', 'sm', 'md', 'lg', 'xl']
|
||||
function parseSize(size: string) {
|
||||
const sizePattern = sizes.join('|')
|
||||
const regex = new RegExp(`^(\\d*)(${sizePattern})$`, 'i')
|
||||
|
||||
const match = size.match(regex)
|
||||
|
||||
if (!match) return null
|
||||
|
||||
const number = match[1] ? Number.parseInt(match[1], 10) : 1 // Default to 1 if no number is present
|
||||
const suffix = match[2] ?? ''
|
||||
|
||||
return { number, suffix }
|
||||
}
|
||||
|
||||
const options = computed(() => {
|
||||
return [...props.schema.schema].sort((a, b) => {
|
||||
const sizeA = parseSize(a)
|
||||
const sizeB = parseSize(b)
|
||||
if (!sizeA || !sizeB) return a.localeCompare(b)
|
||||
|
||||
const suffixAIndex = sizes.indexOf(sizeA.suffix)
|
||||
const suffixBIndex = sizes.indexOf(sizeB.suffix)
|
||||
|
||||
if (suffixAIndex !== -1 && suffixBIndex !== -1) {
|
||||
if (suffixAIndex !== suffixBIndex) {
|
||||
return suffixAIndex - suffixBIndex
|
||||
}
|
||||
} else if (suffixAIndex === -1 || suffixBIndex === -1) {
|
||||
if (sizeA.suffix !== sizeB.suffix) {
|
||||
return sizeA.suffix.localeCompare(sizeB.suffix)
|
||||
}
|
||||
}
|
||||
return sizeA.number - sizeB.number
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu :items="options" class="min-w-[167px]" />
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as z from 'zod'
|
||||
|
||||
export const stringInputSchema = z.literal('string').or(z.string().transform(t => t.split('|').find(s => s.trim() === 'string')).pipe(z.string()))
|
||||
|
||||
export type StringInputSchema = z.infer<typeof stringInputSchema>
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ schema: StringInputSchema }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInput />
|
||||
</template>
|
||||
@@ -1,44 +0,0 @@
|
||||
import type { PropertyMeta } from 'vue-component-meta'
|
||||
import BooleanInput, { booleanInputSchema } from '../components/inputs/BooleanInput.vue'
|
||||
import StringInput, { stringInputSchema } from '../components/inputs/StringInput.vue'
|
||||
import NumberInput, { numberInputSchema } from '../components/inputs/NumberInput.vue'
|
||||
import StringEnumInput, { stringEnumInputSchema } from '../components/inputs/StringEnumInput.vue'
|
||||
import ObjectInput, { objectInputSchema } from '../components/inputs/ObjectInput.vue'
|
||||
import ArrayInput, { arrayInputSchema } from '../components/inputs/ArrayInput.vue'
|
||||
|
||||
// List of available inputs.
|
||||
const availableInputs = [
|
||||
{ id: 'string', schema: stringInputSchema, component: StringInput },
|
||||
{ id: 'number', schema: numberInputSchema, component: NumberInput },
|
||||
{ id: 'boolean', schema: booleanInputSchema, component: BooleanInput },
|
||||
{ id: 'stringEnum', schema: stringEnumInputSchema, component: StringEnumInput },
|
||||
{ id: 'object', schema: objectInputSchema, component: ObjectInput },
|
||||
{ id: 'array', schema: arrayInputSchema, component: ArrayInput }
|
||||
]
|
||||
|
||||
export function usePropSchema() {
|
||||
function resolveInputSchema(schema: PropertyMeta['schema']): { schema: PropertyMeta['schema'], input: any } | undefined {
|
||||
// Return the first input in the list of available inputs that matches the schema.
|
||||
for (const input of availableInputs) {
|
||||
const result = input.schema.safeParse(schema)
|
||||
if (result.success) {
|
||||
// Returns the output from zod to get the transformed output.
|
||||
// It only includes attributes defined in the zod schema.
|
||||
return { schema: result.data as PropertyMeta['schema'], input }
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof schema === 'string') return
|
||||
|
||||
// If the schema is a complex enum or array return the first nested schema that matches an input.
|
||||
if (schema.kind === 'enum' && schema.schema) {
|
||||
const enumSchemas = typeof schema.schema === 'object' ? Object.values(schema.schema) : schema.schema
|
||||
for (const enumSchema of enumSchemas) {
|
||||
const result = resolveInputSchema(enumSchema)
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { resolveInputSchema }
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { createHighlighterCore } from 'shiki/core'
|
||||
import type { BuiltinLanguage, HighlighterCore } from 'shiki'
|
||||
import MaterialThemeLighter from 'shiki/themes/material-theme-lighter.mjs'
|
||||
import MaterialThemePalenight from 'shiki/themes/material-theme-palenight.mjs'
|
||||
import VueLang from 'shiki/langs/vue.mjs'
|
||||
import MarkdownLang from 'shiki/langs/markdown.mjs'
|
||||
import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
|
||||
|
||||
export const highlighter = shallowRef<HighlighterCore>()
|
||||
|
||||
// A custom composable for syntax highlighting with Shiki since `@nuxt/mdc` relies on
|
||||
// a server endpoint to highlight code.
|
||||
export function useShiki() {
|
||||
async function codeToHtml(code: string, lang: BuiltinLanguage | 'text' = 'text') {
|
||||
if (!highlighter.value) {
|
||||
highlighter.value = await createHighlighterCore({
|
||||
themes: [MaterialThemeLighter, MaterialThemePalenight],
|
||||
langs: [VueLang, MarkdownLang],
|
||||
engine: createOnigurumaEngine(import('shiki/wasm'))
|
||||
})
|
||||
}
|
||||
|
||||
return highlighter.value.codeToHtml(code, {
|
||||
lang,
|
||||
themes: {
|
||||
dark: 'material-theme-palenight',
|
||||
default: 'material-theme-lighter',
|
||||
light: 'material-theme-lighter'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { codeToHtml }
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { Options } from 'prettier'
|
||||
import PrettierWorker from '@/workers/prettier.js?worker&inline'
|
||||
|
||||
export interface SimplePrettier {
|
||||
format: (source: string, options?: Options) => Promise<string>
|
||||
}
|
||||
|
||||
function createPrettierWorkerApi(worker: Worker): SimplePrettier {
|
||||
let counter = 0
|
||||
const handlers: any = {}
|
||||
|
||||
worker.addEventListener('message', (event) => {
|
||||
const { uid, message, error } = event.data
|
||||
|
||||
if (!handlers[uid]) {
|
||||
return
|
||||
}
|
||||
|
||||
const [resolve, reject] = handlers[uid]
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete handlers[uid]
|
||||
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(message)
|
||||
}
|
||||
})
|
||||
|
||||
function postMessage<T>(message: any) {
|
||||
const uid = ++counter
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
handlers[uid] = [resolve, reject]
|
||||
worker.postMessage({ uid, message })
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
format(source: string, options?: Options) {
|
||||
return postMessage({ type: 'format', source, options })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin(async () => {
|
||||
const worker = new PrettierWorker()
|
||||
const prettier = createPrettierWorkerApi(worker)
|
||||
|
||||
return {
|
||||
provide: {
|
||||
prettier
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
self.onmessage = async function (event) {
|
||||
self.postMessage({
|
||||
uid: event.data.uid,
|
||||
message: await handleMessage(event.data.message)
|
||||
})
|
||||
}
|
||||
|
||||
function handleMessage(message) {
|
||||
switch (message.type) {
|
||||
case 'format':
|
||||
return handleFormatMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFormatMessage(message) {
|
||||
if (!globalThis.prettier) {
|
||||
await Promise.all([
|
||||
import('https://unpkg.com/prettier@3.5.0/standalone.js'),
|
||||
import('https://unpkg.com/prettier@3.5.0/plugins/html.js'),
|
||||
import('https://unpkg.com/prettier@3.5.0/plugins/postcss.js'),
|
||||
import('https://unpkg.com/prettier@3.5.0/plugins/babel.js'),
|
||||
import('https://unpkg.com/prettier@3.5.0/plugins/estree.js'),
|
||||
import('https://unpkg.com/prettier@3.5.0/plugins/typescript.js')
|
||||
])
|
||||
}
|
||||
|
||||
const { options, source } = message
|
||||
const formatted = await prettier.format(source, {
|
||||
parser: 'vue',
|
||||
plugins: prettierPlugins,
|
||||
...options
|
||||
})
|
||||
|
||||
return formatted
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
|
||||
modules: ['../src/module', '@nuxt/test-utils/module'],
|
||||
|
||||
ssr: false,
|
||||
|
||||
devtools: { enabled: false },
|
||||
|
||||
app: {
|
||||
baseURL: '/__nuxt_ui__/devtools'
|
||||
},
|
||||
|
||||
future: {
|
||||
compatibilityVersion: 4
|
||||
},
|
||||
compatibilityDate: '2024-04-03',
|
||||
|
||||
nitro: {
|
||||
hooks: {
|
||||
'prerender:routes': function (routes) {
|
||||
routes.clear()
|
||||
}
|
||||
},
|
||||
output: {
|
||||
publicDir: resolve(__dirname, '../dist/client/devtools')
|
||||
}
|
||||
},
|
||||
|
||||
vite: {
|
||||
server: {
|
||||
hmr: {
|
||||
clientPort: process.env.PORT ? +process.env.PORT : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "@nuxt/ui-devtools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/ui": "latest",
|
||||
"knitwork": "^1.2.0",
|
||||
"nuxt": "^3.15.4",
|
||||
"prettier": "^3.5.1",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" class="w-auto h-6 shrink-0" viewBox="840 60 180 140"><path d="M958 60.0001H938C933.524 60.0001 929.926 59.9395 927 63C924.074 65.8905 925 67.5792 925 72V141C925 151.372 923.648 156.899 919 162C914.352 166.931 908.468 169 899 169C889.705 169 882.648 166.931 878 162C873.352 156.899 873 151.372 873 141V72.0001C873 67.5793 872.926 65.8906 870 63.0001C867.074 59.9396 863.476 60.0001 859 60.0001H840V141C840 159.023 845.016 173.458 855 184C865.156 194.542 879.893 200 899 200C918.107 200 932.844 194.542 943 184C953.156 173.458 958 159.023 958 141V60.0001Z" fill="currentColor"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M1000 60.0233L1020 60V77L1020 128V156.007L1020 181L1020 189.004C1020 192.938 1019.98 194.429 1017 197.001C1014.02 199.725 1009.56 200 1005 200H986.001V181.006L986 130.012V70.0215C986 66.1576 986.016 64.5494 989 62.023C991.819 59.6358 995.437 60.0233 1000 60.0233Z" fill="currentColor"></path> <style> path { fill: #00000080; } @media (prefers-color-scheme: dark) { path { fill: #ffffff80; } } </style> </svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,24 +0,0 @@
|
||||
// @vitest-environment node
|
||||
import { it, expect, describe } from 'vitest'
|
||||
import { usePropSchema } from '../../app/composables/usePropSchema'
|
||||
import { stringSchema, optionalStringSchema, booleanSchema, numberSchema, optionalNumberSchema, optionalBooleanSchema, objectSchema, arraySchema, arrayOptionalSchema, stringEnumSchema } from '../fixtures/schemas'
|
||||
|
||||
describe('usePropSchema', () => {
|
||||
const { resolveInputSchema } = usePropSchema()
|
||||
|
||||
it.each([
|
||||
['string', { schema: stringSchema, inputId: 'string' }],
|
||||
['optional string', { schema: optionalStringSchema, inputId: 'string' }],
|
||||
['number', { schema: numberSchema, inputId: 'number' }],
|
||||
['optional number', { schema: optionalNumberSchema, inputId: 'number' }],
|
||||
['boolean', { schema: booleanSchema, inputId: 'boolean' }],
|
||||
['string enum', { schema: stringEnumSchema, inputId: 'stringEnum' }],
|
||||
['object', { schema: objectSchema, inputId: 'object' }],
|
||||
['optional boolean', { schema: optionalBooleanSchema, inputId: 'boolean' }],
|
||||
['array', { schema: arraySchema, inputId: 'array' }],
|
||||
['optional array', { schema: arrayOptionalSchema, inputId: 'array' }]
|
||||
])('resolveInputSchema should resolve %s schema', async (_: string, options) => {
|
||||
const result = resolveInputSchema(options.schema as any)
|
||||
expect(result?.input.id).toBe(options.inputId)
|
||||
})
|
||||
})
|
||||
133
devtools/test/fixtures/schemas.ts
vendored
133
devtools/test/fixtures/schemas.ts
vendored
@@ -1,133 +0,0 @@
|
||||
export const stringSchema = 'string' as const
|
||||
|
||||
export const optionalStringSchema = {
|
||||
kind: 'enum',
|
||||
type: 'string | undefined',
|
||||
schema: {
|
||||
0: 'undefined',
|
||||
1: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
export const numberSchema = 'number' as const
|
||||
export const optionalNumberSchema = {
|
||||
kind: 'enum',
|
||||
type: 'number | undefined',
|
||||
schema: {
|
||||
0: 'undefined',
|
||||
1: 'number'
|
||||
}
|
||||
}
|
||||
|
||||
export const booleanSchema = 'boolean' as const
|
||||
export const optionalBooleanSchema = {
|
||||
kind: 'enum',
|
||||
type: 'boolean | undefined',
|
||||
schema: {
|
||||
0: 'undefined',
|
||||
1: 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
export const objectSchema = {
|
||||
kind: 'object',
|
||||
type: 'AccordionItem',
|
||||
schema: {
|
||||
label: {
|
||||
name: 'label',
|
||||
global: false,
|
||||
description: '',
|
||||
tags: [],
|
||||
required: false,
|
||||
type: 'string | undefined',
|
||||
schema: {
|
||||
kind: 'enum',
|
||||
type: 'string | undefined',
|
||||
schema: {
|
||||
0: 'undefined',
|
||||
1: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const arraySchema = {
|
||||
kind: 'array',
|
||||
type: 'AccordionItem[]',
|
||||
schema: [
|
||||
{
|
||||
kind: 'object',
|
||||
type: 'AccordionItem',
|
||||
schema: {
|
||||
label: {
|
||||
name: 'label',
|
||||
global: false,
|
||||
description: '',
|
||||
tags: [],
|
||||
required: false,
|
||||
type: 'string | undefined',
|
||||
schema: {
|
||||
kind: 'enum',
|
||||
type: 'string | undefined',
|
||||
schema: {
|
||||
0: 'undefined',
|
||||
1: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const arrayOptionalSchema = {
|
||||
kind: 'enum',
|
||||
type: 'AccordionItem[] | undefined',
|
||||
schema: {
|
||||
0: 'undefined',
|
||||
1: {
|
||||
kind: 'array',
|
||||
type: 'AccordionItem[]',
|
||||
schema: [
|
||||
{
|
||||
kind: 'object',
|
||||
type: 'AccordionItem',
|
||||
schema: {
|
||||
label: {
|
||||
name: 'label',
|
||||
global: false,
|
||||
description: '',
|
||||
tags: [],
|
||||
required: false,
|
||||
type: 'string | undefined',
|
||||
schema: {
|
||||
kind: 'enum',
|
||||
type: 'string | undefined',
|
||||
schema: {
|
||||
0: 'undefined',
|
||||
1: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const stringEnumSchema = {
|
||||
kind: 'enum',
|
||||
type: '"true" | "false" | "page" | "step" | "location" | "date" | "time" | undefined',
|
||||
schema: {
|
||||
0: 'undefined',
|
||||
1: '"true"',
|
||||
2: '"false"',
|
||||
3: '"page"',
|
||||
4: '"step"',
|
||||
5: '"location"',
|
||||
6: '"date"',
|
||||
7: '"time"'
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineVitestConfig } from '@nuxt/test-utils/config'
|
||||
|
||||
export default defineVitestConfig({
|
||||
test: {
|
||||
environment: 'nuxt'
|
||||
}
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
@@ -93,3 +93,7 @@ provide('navigation', mappedNavigation)
|
||||
</template>
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Safelist (do not remove): [&>div]:*:my-0 [&>div]:*:w-full h-64 !px-0 !py-0 !pt-0 !pb-0 !p-0 !justify-start !min-h-96 h-136 */
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss" theme(static) source("../../../..");
|
||||
@import "@nuxt/ui-pro";
|
||||
|
||||
@source "../../../content";
|
||||
@source "../../../node_modules/.c12";
|
||||
|
||||
@theme {
|
||||
@theme static {
|
||||
--container-8xl: 90rem;
|
||||
|
||||
--font-sans: 'Public Sans', sans-serif;
|
||||
@@ -31,5 +32,3 @@ html[data-module="ui-pro"] .ui-only,
|
||||
html[data-module="ui"] .ui-pro-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Safelist (do not remove): [&>div]:*:my-0 [&>div]:*:w-full h-64 !px-0 !py-0 !pt-0 !pb-0 !p-0 !justify-start !min-h-96 h-136 */
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
const links = [{
|
||||
label: 'Figma',
|
||||
to: '/figma'
|
||||
@@ -16,7 +18,7 @@ const links = [{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USeparator icon="i-simple-icons-nuxtdotjs" class="h-px" />
|
||||
<USeparator :icon="route.path === '/' ? undefined : 'i-simple-icons-nuxtdotjs'" class="h-px" />
|
||||
|
||||
<UFooter>
|
||||
<template #left>
|
||||
|
||||
@@ -6,6 +6,7 @@ const props = defineProps<{
|
||||
links: NavigationMenuItem[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const config = useRuntimeConfig().public
|
||||
const { module } = useSharedData()
|
||||
|
||||
@@ -21,15 +22,20 @@ onMounted(() => {
|
||||
|
||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
||||
|
||||
const items = computed(() => props.links.map(({ icon, ...link }) => link))
|
||||
const desktopLinks = computed(() => props.links.map(({ icon, ...link }) => link))
|
||||
const mobileLinks = computed(() => props.links.map(link => ({ ...link, defaultOpen: link.children && route.path.startsWith(link.to as string) })))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UHeader :ui="{ left: 'min-w-0' }" mode="drawer" :menu="{ shouldScaleBackground: true }">
|
||||
<UHeader :ui="{ left: 'min-w-0' }" :menu="{ shouldScaleBackground: true }">
|
||||
<template #left>
|
||||
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-(--ui-text-highlighted) min-w-0 focus-visible:outline-(--ui-primary) shrink-0" aria-label="Nuxt UI">
|
||||
<LogoPro class="w-auto h-6 shrink-0 ui-pro-only" />
|
||||
<Logo class="w-auto h-6 shrink-0 ui-only" />
|
||||
<Logo v-if="route.path === '/'" class="w-auto h-6 shrink-0" />
|
||||
<LogoPro v-else-if="route.path.startsWith('/pro')" class="w-auto h-6 shrink-0" />
|
||||
<template v-else>
|
||||
<LogoPro class="w-auto h-6 shrink-0 ui-pro-only" />
|
||||
<Logo class="w-auto h-6 shrink-0 ui-only" />
|
||||
</template>
|
||||
</NuxtLink>
|
||||
|
||||
<UDropdownMenu
|
||||
@@ -53,7 +59,7 @@ const items = computed(() => props.links.map(({ icon, ...link }) => link))
|
||||
</UDropdownMenu>
|
||||
</template>
|
||||
|
||||
<UNavigationMenu :items="items" variant="link" />
|
||||
<UNavigationMenu :items="desktopLinks" variant="link" />
|
||||
|
||||
<template #right>
|
||||
<ThemePicker />
|
||||
@@ -64,6 +70,7 @@ const items = computed(() => props.links.map(({ icon, ...link }) => link))
|
||||
|
||||
<UTooltip text="Open on GitHub" class="hidden lg:flex">
|
||||
<UButton
|
||||
:key="value"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
:to="`https://github.com/nuxt/${value}`"
|
||||
@@ -75,7 +82,7 @@ const items = computed(() => props.links.map(({ icon, ...link }) => link))
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UNavigationMenu orientation="vertical" :items="links" class="-mx-2.5" />
|
||||
<UNavigationMenu orientation="vertical" :items="mobileLinks" class="-mx-2.5" default-open />
|
||||
|
||||
<USeparator type="dashed" class="mt-4 mb-6" />
|
||||
|
||||
@@ -89,7 +96,7 @@ const items = computed(() => props.links.map(({ icon, ...link }) => link))
|
||||
<span class="inline-flex items-center gap-0.5">
|
||||
{{ link.title }}
|
||||
|
||||
<sup v-if="link.module === 'ui-pro' && link.path.startsWith('/components')" class="text-[8px] font-medium text-(--ui-primary)">PRO</sup>
|
||||
<sup v-if="link.module === 'ui-pro'" class="text-[8px] font-medium text-(--ui-primary)">PRO</sup>
|
||||
</span>
|
||||
</template>
|
||||
</UContentNavigation>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<UPageCard
|
||||
variant="subtle"
|
||||
class="rounded-[calc(var(--ui-radius)*6)]"
|
||||
>
|
||||
<video
|
||||
class="rounded-[calc(var(--ui-radius)*2)]"
|
||||
preload="none"
|
||||
poster="https://res.cloudinary.com/nuxt/video/upload/so_3.3/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.jpg"
|
||||
:controls="true"
|
||||
>
|
||||
<source
|
||||
src="https://res.cloudinary.com/nuxt/video/upload/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.webm"
|
||||
type="video/webm"
|
||||
>
|
||||
<source
|
||||
src="https://res.cloudinary.com/nuxt/video/upload/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.mp4"
|
||||
type="video/mp4"
|
||||
>
|
||||
<source
|
||||
src="https://res.cloudinary.com/nuxt/video/upload/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.ogg"
|
||||
type="video/ogg"
|
||||
>
|
||||
</video>
|
||||
</UPageCard>
|
||||
</div>
|
||||
</template>
|
||||
91
docs/app/components/SkyBg.vue
Normal file
91
docs/app/components/SkyBg.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
interface Star {
|
||||
x: number
|
||||
y: number
|
||||
size: number
|
||||
twinkleDelay: number
|
||||
id: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
starCount?: number
|
||||
color?: string
|
||||
size?: { min: number, max: number }
|
||||
speed?: 'slow' | 'normal' | 'fast'
|
||||
}>(), {
|
||||
starCount: 50,
|
||||
color: 'var(--ui-primary)',
|
||||
size: () => ({
|
||||
min: 1,
|
||||
max: 3
|
||||
}),
|
||||
speed: 'normal'
|
||||
})
|
||||
|
||||
// Generate random stars
|
||||
const generateStars = (count: number): Star[] => {
|
||||
return Array.from({ length: count }, () => {
|
||||
const x = Math.floor(Math.random() * 100)
|
||||
const y = Math.floor(Math.random() * 100)
|
||||
const size = Math.random() * (props.size.max - props.size.min) + props.size.min
|
||||
const twinkleDelay = Math.random() * 5
|
||||
|
||||
return { x, y, size, twinkleDelay, id: Math.random().toString(36).substring(2, 9) }
|
||||
})
|
||||
}
|
||||
|
||||
// Generate all stars
|
||||
const stars = ref<Star[]>(generateStars(props.starCount))
|
||||
|
||||
// Compute twinkle animation duration based on speed
|
||||
const twinkleDuration = computed(() => {
|
||||
const speedMap: Record<string, string> = {
|
||||
slow: '4s',
|
||||
normal: '2s',
|
||||
fast: '1s'
|
||||
}
|
||||
return speedMap[props.speed]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute pointer-events-none z-[-1] inset-y-0 left-4 right-4 lg:right-[50%] overflow-hidden">
|
||||
<ClientOnly>
|
||||
<div
|
||||
v-for="star in stars"
|
||||
:key="star.id"
|
||||
class="star absolute"
|
||||
:style="{
|
||||
'left': `${star.x}%`,
|
||||
'top': `${star.y}%`,
|
||||
'transform': 'translate(-50%, -50%)',
|
||||
'--star-size': `${star.size}px`,
|
||||
'--star-color': color,
|
||||
'--twinkle-delay': `${star.twinkleDelay}s`,
|
||||
'--twinkle-duration': twinkleDuration
|
||||
}"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.star {
|
||||
width: var(--star-size);
|
||||
height: var(--star-size);
|
||||
background-color: var(--star-color);
|
||||
border-radius: 50%;
|
||||
animation: twinkle var(--twinkle-duration) ease-in-out infinite;
|
||||
animation-delay: var(--twinkle-delay);
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because one or more lines are too long
@@ -84,7 +84,7 @@ const props = defineProps<{
|
||||
const route = useRoute()
|
||||
const { $prettier } = useNuxtApp()
|
||||
|
||||
const camelName = camelCase(props.slug ?? route.params.slug?.[route.params.slug.length - 1] ?? '')
|
||||
const camelName = camelCase(props.slug ?? route.path.split('/').pop() ?? '')
|
||||
const name = `${props.prose ? 'Prose' : 'U'}${upperFirst(camelName)}`
|
||||
const component = defineAsyncComponent(() => {
|
||||
if (props.pro) {
|
||||
@@ -316,7 +316,7 @@ const { data: ast } = await useAsyncData(`component-code-${name}-${hash({ props:
|
||||
<template>
|
||||
<div class="my-5">
|
||||
<div>
|
||||
<div v-if="options.length" class="flex items-center gap-2.5 border border-(--ui-border-muted) border-b-0 relative rounded-t-[calc(var(--ui-radius)*1.5)] px-4 py-2.5 overflow-x-auto">
|
||||
<div v-if="options.length" class="flex flex-wrap items-center gap-2.5 border border-(--ui-border-muted) border-b-0 relative rounded-t-[calc(var(--ui-radius)*1.5)] px-4 py-2.5 overflow-x-auto">
|
||||
<template v-for="option in options" :key="option.name">
|
||||
<UFormField
|
||||
:label="option.label"
|
||||
|
||||
@@ -7,7 +7,7 @@ const props = defineProps<{
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const camelName = camelCase(route.params.slug?.[route.params.slug.length - 1] ?? '')
|
||||
const camelName = camelCase(route.path.split('/').pop() ?? '')
|
||||
const name = props.prose ? `Prose${upperFirst(camelName)}` : `U${upperFirst(camelName)}`
|
||||
|
||||
const meta = await fetchComponentMeta(name as any)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { upperFirst, camelCase } from 'scule'
|
||||
import { upperFirst, camelCase, kebabCase } from 'scule'
|
||||
import type { ComponentMeta } from 'vue-component-meta'
|
||||
import * as theme from '#build/ui'
|
||||
import * as themePro from '#build/ui-pro'
|
||||
@@ -34,7 +34,7 @@ const props = withDefaults(defineProps<{
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const camelName = camelCase(props.name ?? route.params.slug?.[route.params.slug.length - 1] ?? '')
|
||||
const camelName = camelCase(props.name ?? route.path.split('/').pop() ?? '')
|
||||
const componentName = props.prose ? `Prose${upperFirst(camelName)}` : `U${upperFirst(camelName)}`
|
||||
|
||||
const componentTheme = ((props.pro ? props.prose ? themePro.prose : themePro : theme) as any)[camelName]
|
||||
@@ -112,7 +112,7 @@ const metaProps: ComputedRef<ComponentMeta['props']> = computed(() => {
|
||||
<ProseTd>
|
||||
<HighlightInlineType v-if="prop.type" :type="prop.type" />
|
||||
|
||||
<MDC v-if="prop.description" :value="prop.description" class="text-(--ui-text-toned) mt-1" />
|
||||
<MDC v-if="prop.description" :value="prop.description" class="text-(--ui-text-toned) mt-1" :cache-key="`${kebabCase(route.path)}-${prop.name}-description`" />
|
||||
|
||||
<ComponentPropsLinks v-if="prop.tags?.length" :prop="prop" />
|
||||
<ComponentPropsSchema v-if="prop.schema" :prop="prop" :ignore="ignore" />
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { kebabCase } from 'scule'
|
||||
import type { PropertyMeta } from 'vue-component-meta'
|
||||
|
||||
const props = defineProps<{
|
||||
prop: PropertyMeta
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const links = computed(() => props.prop.tags?.filter((tag: any) => tag.name === 'link'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProseUl v-if="links?.length">
|
||||
<ProseLi v-for="link in links" :key="link.name">
|
||||
<MDC :value="link.text ?? ''" class="my-1" />
|
||||
<ProseLi v-for="(link, index) in links" :key="index">
|
||||
<MDC :value="link.text ?? ''" class="my-1" :cache-key="`${kebabCase(route.path)}-${prop.name}-link-${index}`" />
|
||||
</ProseLi>
|
||||
</ProseUl>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { kebabCase } from 'scule'
|
||||
import type { PropertyMeta } from 'vue-component-meta'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -6,6 +7,8 @@ const props = defineProps<{
|
||||
ignore?: string[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
function getSchemaProps(schema: PropertyMeta['schema']): any {
|
||||
if (!schema || typeof schema === 'string' || !schema.schema) {
|
||||
return []
|
||||
@@ -40,7 +43,7 @@ const schemaProps = computed(() => {
|
||||
<ProseLi v-for="schemaProp in schemaProps" :key="schemaProp.name">
|
||||
<HighlightInlineType :type="`${schemaProp.name}${schemaProp.required === false ? '?' : ''}: ${schemaProp.type}`" />
|
||||
|
||||
<MDC v-if="schemaProp.description" :value="schemaProp.description" class="text-(--ui-text-muted) my-1" />
|
||||
<MDC v-if="schemaProp.description" :value="schemaProp.description" class="text-(--ui-text-muted) my-1" :cache-key="`${kebabCase(route.path)}-${prop.name}-${schemaProp.name}-description`" />
|
||||
</ProseLi>
|
||||
</ProseUl>
|
||||
</ProseCollapsible>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { upperFirst, camelCase } from 'scule'
|
||||
import { upperFirst, camelCase, kebabCase } from 'scule'
|
||||
|
||||
const props = defineProps<{
|
||||
prose?: boolean
|
||||
@@ -8,7 +8,7 @@ const props = defineProps<{
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const camelName = camelCase(props.slug ?? route.params.slug?.[route.params.slug.length - 1] ?? '')
|
||||
const camelName = camelCase(props.slug ?? route.path.split('/').pop() ?? '')
|
||||
const name = `${props.prose ? 'Prose' : 'U'}${upperFirst(camelName)}`
|
||||
|
||||
const meta = await fetchComponentMeta(name as any)
|
||||
@@ -36,7 +36,7 @@ const meta = await fetchComponentMeta(name as any)
|
||||
<ProseTd>
|
||||
<HighlightInlineType v-if="slot.type" :type="slot.type" />
|
||||
|
||||
<MDC v-if="slot.description" :value="slot.description" class="text-(--ui-text-toned) mt-1" />
|
||||
<MDC v-if="slot.description" :value="slot.description" class="text-(--ui-text-toned) mt-1" :cache-key="`${kebabCase(route.path)}-${slot.name}-description`" />
|
||||
</ProseTd>
|
||||
</ProseTr>
|
||||
</ProseTbody>
|
||||
|
||||
@@ -15,7 +15,7 @@ const props = defineProps<{
|
||||
const route = useRoute()
|
||||
const { framework } = useSharedData()
|
||||
|
||||
const name = props.slug ?? route.params.slug?.[route.params.slug.length - 1] ?? ''
|
||||
const name = props.slug ?? route.path.split('/').pop() ?? ''
|
||||
const camelName = camelCase(name)
|
||||
|
||||
const strippedCompoundVariants = ref(false)
|
||||
@@ -96,7 +96,7 @@ export default defineAppConfig(${json5.stringify(component.value, null, 2).repla
|
||||
|
||||
::
|
||||
|
||||
::code-collapse{class="vue-only"}
|
||||
::code-collapse{class="vue-only ui-only"}
|
||||
|
||||
\`\`\`ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
@@ -116,6 +116,26 @@ export default defineConfig({
|
||||
|
||||
::
|
||||
|
||||
::code-collapse{class="vue-only ui-pro-only"}
|
||||
|
||||
\`\`\`ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import uiPro from '@nuxt/ui-pro/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
uiPro(${json5.stringify(component.value, null, 2).replace(/,([ |\t\n]+[}|\])])/g, '$1')
|
||||
.split('\n')
|
||||
.map((line, i) => i === 0 ? line : ` ${line}`)
|
||||
.join('\n')})
|
||||
]
|
||||
})
|
||||
\`\`\`
|
||||
|
||||
::
|
||||
|
||||
${strippedCompoundVariants.value
|
||||
? `
|
||||
::callout{icon="i-simple-icons-github" to="${themeLink.value}" title="Compound variants"}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { murmurHash } from 'ohash'
|
||||
import { hash } from 'ohash'
|
||||
|
||||
const props = defineProps<{
|
||||
type: string
|
||||
@@ -23,7 +23,7 @@ const type = computed(() => {
|
||||
return type
|
||||
})
|
||||
|
||||
const { data: ast } = await useAsyncData(`hightlight-inline-code-${murmurHash(type.value)}`, () => parseMarkdown(`\`${type.value}\`{lang="ts-type"}`))
|
||||
const { data: ast } = await useAsyncData(`hightlight-inline-code-${hash(type.value).slice(0, 10)}`, () => parseMarkdown(`\`${type.value}\`{lang="ts-type"}`))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -31,11 +31,7 @@ export default defineAppConfig(${json5.stringify({
|
||||
|
||||
::
|
||||
|
||||
::caution{class="ui-pro-only vue-only"}
|
||||
Nuxt UI Pro v3 does not support Vue yet.
|
||||
::
|
||||
|
||||
::code-collapse{class="vue-only"}
|
||||
::code-collapse{class="vue-only ui-only"}
|
||||
|
||||
\`\`\`ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
@@ -57,6 +53,30 @@ export default defineConfig({
|
||||
})
|
||||
\`\`\`
|
||||
|
||||
::
|
||||
|
||||
::code-collapse{class="vue-only ui-pro-only"}
|
||||
|
||||
\`\`\`ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import uiPro from '@nuxt/ui-pro/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
uiPro(${json5.stringify({
|
||||
ui: {
|
||||
icons
|
||||
}
|
||||
}, null, 2).replace(/,([ |\t\n]+[}|\])])/g, '$1')
|
||||
.split('\n')
|
||||
.map((line, i) => i === 0 ? line : ` ${line}`)
|
||||
.join('\n')})
|
||||
]
|
||||
})
|
||||
\`\`\`
|
||||
|
||||
::
|
||||
`
|
||||
|
||||
|
||||
@@ -9,21 +9,22 @@ const props = withDefaults(defineProps<{
|
||||
|
||||
function getEmojiFlag(locale: string): string {
|
||||
const languageToCountry: Record<string, string> = {
|
||||
ar: 'sa',
|
||||
bn: 'bd',
|
||||
cs: 'cz',
|
||||
da: 'dk',
|
||||
el: 'gr',
|
||||
et: 'ee',
|
||||
en: 'gb',
|
||||
hi: 'in',
|
||||
ja: 'jp',
|
||||
km: 'kh',
|
||||
ko: 'kr',
|
||||
nb: 'no',
|
||||
sv: 'se',
|
||||
uk: 'ua',
|
||||
vi: 'vn'
|
||||
ar: 'sa', // Arabic -> Saudi Arabia
|
||||
bn: 'bd', // Bengali -> Bangladesh
|
||||
cs: 'cz', // Czech -> Czech Republic (note: modern country code is actually 'cz')
|
||||
da: 'dk', // Danish -> Denmark
|
||||
el: 'gr', // Greek -> Greece
|
||||
et: 'ee', // Estonian -> Estonia
|
||||
en: 'gb', // English -> Great Britain
|
||||
he: 'il', // Hebrew -> Israel
|
||||
hi: 'in', // Hindi -> India
|
||||
ja: 'jp', // Japanese -> Japan
|
||||
km: 'kh', // Khmer -> Cambodia
|
||||
ko: 'kr', // Korean -> South Korea
|
||||
nb: 'no', // Norwegian Bokmål -> Norway
|
||||
sv: 'se', // Swedish -> Sweden
|
||||
uk: 'ua', // Ukrainian -> Ukraine
|
||||
vi: 'vn' // Vietnamese -> Vietnam
|
||||
}
|
||||
|
||||
const baseLanguage = locale.split('-')[0]?.toLowerCase() || locale
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarDate, HebrewCalendar } from '@internationalized/date'
|
||||
|
||||
const hebrewDate = shallowRef(new CalendarDate(new HebrewCalendar(), 5781, 1, 1))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCalendar v-model="hebrewDate" />
|
||||
</template>
|
||||
@@ -2,6 +2,7 @@
|
||||
const searchTerm = ref('')
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'command-palette-users',
|
||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const { data: users } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'command-palette-users',
|
||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ const searchTerm = ref('')
|
||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'command-palette-users',
|
||||
params: { q: searchTermDebounced },
|
||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
|
||||
|
||||
@@ -11,7 +11,7 @@ const items = [
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
id: '/getting-started#reka-ui-radix-vue',
|
||||
id: '/getting-started#reka-ui',
|
||||
label: 'Reka UI',
|
||||
level: 3
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const searchTerm = ref('')
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'command-palette-users',
|
||||
params: { q: searchTerm },
|
||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users',
|
||||
transform: (data: { id: number, name: string }[]) => {
|
||||
return data?.map(user => ({
|
||||
label: user.name,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users-email',
|
||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||
return data?.map(user => ({
|
||||
label: user.name,
|
||||
|
||||
@@ -3,6 +3,7 @@ const searchTerm = ref('')
|
||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users',
|
||||
params: { q: searchTermDebounced },
|
||||
transform: (data: { id: number, name: string }[]) => {
|
||||
return data?.map(user => ({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const searchTerm = ref('')
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'command-palette-users',
|
||||
params: { q: searchTerm },
|
||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
const modal = useModal()
|
||||
|
||||
defineProps<{
|
||||
count: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
function onSuccess() {
|
||||
emit('success')
|
||||
}
|
||||
const emit = defineEmits<{ close: [boolean] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :title="`This modal was opened programmatically ${count} times`">
|
||||
<UModal :close="{ onClick: () => emit('close', false) }" :title="`This modal was opened programmatically ${count} times`">
|
||||
<template #footer>
|
||||
<div class="flex gap-2">
|
||||
<UButton color="neutral" label="Close" @click="modal.close()" />
|
||||
<UButton label="Success" @click="onSuccess" />
|
||||
<UButton color="neutral" label="Dismiss" @click="emit('close', false)" />
|
||||
<UButton label="Success" @click="emit('close', true)" />
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
@@ -4,20 +4,37 @@ import { LazyModalExample } from '#components'
|
||||
const count = ref(0)
|
||||
|
||||
const toast = useToast()
|
||||
const modal = useModal()
|
||||
const overlay = useOverlay()
|
||||
|
||||
function open() {
|
||||
count.value++
|
||||
const modal = overlay.create(LazyModalExample, {
|
||||
props: {
|
||||
count: count.value
|
||||
}
|
||||
})
|
||||
|
||||
modal.open(LazyModalExample, {
|
||||
description: 'And you can even provide a description!',
|
||||
count: count.value,
|
||||
onSuccess() {
|
||||
toast.add({
|
||||
title: 'Success !',
|
||||
id: 'modal-success'
|
||||
})
|
||||
}
|
||||
async function open() {
|
||||
const shouldIncrement = await modal.open()
|
||||
|
||||
if (shouldIncrement) {
|
||||
count.value++
|
||||
|
||||
toast.add({
|
||||
title: `Success: ${shouldIncrement}`,
|
||||
color: 'success',
|
||||
id: 'modal-success'
|
||||
})
|
||||
|
||||
// Update the count
|
||||
modal.patch({
|
||||
count: count.value
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
toast.add({
|
||||
title: `Dismissed: ${shouldIncrement}`,
|
||||
color: 'error',
|
||||
id: 'modal-dismiss'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -41,14 +41,9 @@ const items = [
|
||||
description: 'Define shortcuts for your application.'
|
||||
},
|
||||
{
|
||||
label: 'useModal',
|
||||
label: 'useOverlay',
|
||||
icon: 'i-lucide-file-text',
|
||||
description: 'Display a modal within your application.'
|
||||
},
|
||||
{
|
||||
label: 'useSlideover',
|
||||
icon: 'i-lucide-file-text',
|
||||
description: 'Display a slideover within your application.'
|
||||
description: 'Display a modal/slideover within your application.'
|
||||
},
|
||||
{
|
||||
label: 'useToast',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users',
|
||||
transform: (data: { id: number, name: string }[]) => {
|
||||
return data?.map(user => ({
|
||||
label: user.name,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users-email',
|
||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||
return data?.map(user => ({
|
||||
label: user.name,
|
||||
|
||||
@@ -3,6 +3,7 @@ const searchTerm = ref('')
|
||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users',
|
||||
params: { q: searchTermDebounced },
|
||||
transform: (data: { id: number, name: string }[]) => {
|
||||
return data?.map(user => ({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users',
|
||||
transform: (data: { id: number, name: string }[]) => {
|
||||
return data?.map(user => ({
|
||||
label: user.name,
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
const slideover = useSlideover()
|
||||
|
||||
defineProps<{
|
||||
count: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
function onSuccess() {
|
||||
emit('success')
|
||||
}
|
||||
const emit = defineEmits<{ close: [boolean] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USlideover :description="`This slideover was opened programmatically ${count} times`">
|
||||
<USlideover :close="{ onClick: () => emit('close', false) }" :description="`This slideover was opened programmatically ${count} times`">
|
||||
<template #body>
|
||||
<Placeholder class="h-full" />
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex gap-2">
|
||||
<UButton color="neutral" label="Close" @click="slideover.close()" />
|
||||
<UButton label="Success" @click="onSuccess" />
|
||||
<UButton color="neutral" label="Dismiss" @click="emit('close', false)" />
|
||||
<UButton label="Success" @click="emit('close', true)" />
|
||||
</div>
|
||||
</template>
|
||||
</USlideover>
|
||||
|
||||
@@ -4,20 +4,37 @@ import { LazySlideoverExample } from '#components'
|
||||
const count = ref(0)
|
||||
|
||||
const toast = useToast()
|
||||
const slideover = useSlideover()
|
||||
const overlay = useOverlay()
|
||||
|
||||
function open() {
|
||||
count.value++
|
||||
const slideover = overlay.create(LazySlideoverExample, {
|
||||
props: {
|
||||
count: count.value
|
||||
}
|
||||
})
|
||||
|
||||
slideover.open(LazySlideoverExample, {
|
||||
title: 'Slideover',
|
||||
count: count.value,
|
||||
onSuccess() {
|
||||
toast.add({
|
||||
title: 'Success !',
|
||||
id: 'modal-success'
|
||||
})
|
||||
}
|
||||
async function open() {
|
||||
const shouldIncrement = await slideover.open()
|
||||
|
||||
if (shouldIncrement) {
|
||||
count.value++
|
||||
|
||||
toast.add({
|
||||
title: `Success: ${shouldIncrement}`,
|
||||
color: 'success',
|
||||
id: 'slideover-success'
|
||||
})
|
||||
|
||||
// Update the count
|
||||
slideover.patch({
|
||||
count: count.value
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
toast.add({
|
||||
title: `Dismissed: ${shouldIncrement}`,
|
||||
color: 'error',
|
||||
id: 'slideover-dismiss'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -13,6 +13,7 @@ type User = {
|
||||
}
|
||||
|
||||
const { data, status } = await useFetch<User[]>('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'table-users',
|
||||
transform: (data) => {
|
||||
return data?.map(user => ({
|
||||
...user,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { h, resolveComponent } from 'vue'
|
||||
import type { TableColumn, TableRow } from '@nuxt/ui'
|
||||
|
||||
const UBadge = resolveComponent('UBadge')
|
||||
const UCheckbox = resolveComponent('UCheckbox')
|
||||
|
||||
type Payment = {
|
||||
id: string
|
||||
date: string
|
||||
status: 'paid' | 'failed' | 'refunded'
|
||||
email: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
const data = ref<Payment[]>([{
|
||||
id: '4600',
|
||||
date: '2024-03-11T15:30:00',
|
||||
status: 'paid',
|
||||
email: 'james.anderson@example.com',
|
||||
amount: 594
|
||||
}, {
|
||||
id: '4599',
|
||||
date: '2024-03-11T10:10:00',
|
||||
status: 'failed',
|
||||
email: 'mia.white@example.com',
|
||||
amount: 276
|
||||
}, {
|
||||
id: '4598',
|
||||
date: '2024-03-11T08:50:00',
|
||||
status: 'refunded',
|
||||
email: 'william.brown@example.com',
|
||||
amount: 315
|
||||
}, {
|
||||
id: '4597',
|
||||
date: '2024-03-10T19:45:00',
|
||||
status: 'paid',
|
||||
email: 'emma.davis@example.com',
|
||||
amount: 529
|
||||
}, {
|
||||
id: '4596',
|
||||
date: '2024-03-10T15:55:00',
|
||||
status: 'paid',
|
||||
email: 'ethan.harris@example.com',
|
||||
amount: 639
|
||||
}])
|
||||
|
||||
const columns: TableColumn<Payment>[] = [{
|
||||
id: 'select',
|
||||
header: ({ table }) => h(UCheckbox, {
|
||||
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
|
||||
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
|
||||
'ariaLabel': 'Select all'
|
||||
}),
|
||||
cell: ({ row }) => h(UCheckbox, {
|
||||
'modelValue': row.getIsSelected(),
|
||||
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
|
||||
'ariaLabel': 'Select row'
|
||||
})
|
||||
}, {
|
||||
accessorKey: 'date',
|
||||
header: 'Date',
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.getValue('date')).toLocaleString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
}, {
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => {
|
||||
const color = ({
|
||||
paid: 'success' as const,
|
||||
failed: 'error' as const,
|
||||
refunded: 'neutral' as const
|
||||
})[row.getValue('status') as string]
|
||||
|
||||
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
|
||||
}
|
||||
}, {
|
||||
accessorKey: 'email',
|
||||
header: 'Email'
|
||||
}, {
|
||||
accessorKey: 'amount',
|
||||
header: () => h('div', { class: 'text-right' }, 'Amount'),
|
||||
cell: ({ row }) => {
|
||||
const amount = Number.parseFloat(row.getValue('amount'))
|
||||
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount)
|
||||
|
||||
return h('div', { class: 'text-right font-medium' }, formatted)
|
||||
}
|
||||
}]
|
||||
|
||||
const table = useTemplateRef('table')
|
||||
|
||||
const rowSelection = ref<Record<string, boolean>>({ })
|
||||
|
||||
function onSelect(row: TableRow<Payment>, e?: Event) {
|
||||
/* If you decide to also select the column you can do this */
|
||||
row.toggleSelected(!row.getIsSelected())
|
||||
|
||||
console.log(e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class=" flex w-full flex-1 gap-1">
|
||||
<div class="flex-1">
|
||||
<UTable
|
||||
ref="table"
|
||||
v-model:row-selection="rowSelection"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
@select="onSelect"
|
||||
/>
|
||||
|
||||
<div class="px-4 py-3.5 border-t border-[var(--ui-border-accented)] text-sm text-[var(--ui-text-muted)]">
|
||||
{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
|
||||
{{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeItem } from '@nuxt/ui'
|
||||
|
||||
const items: TreeItem[] = [
|
||||
{
|
||||
label: 'app/',
|
||||
slot: 'app',
|
||||
defaultExpanded: true,
|
||||
children: [{
|
||||
label: 'composables/',
|
||||
children: [
|
||||
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
|
||||
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'components/',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
|
||||
]
|
||||
}]
|
||||
},
|
||||
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTree :items="items">
|
||||
<template #app="{ item }">
|
||||
<p class="italic font-bold">
|
||||
{{ item.label }}
|
||||
</p>
|
||||
</template>
|
||||
</UTree>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeItem } from '@nuxt/ui'
|
||||
|
||||
const items: TreeItem[] = [
|
||||
{
|
||||
label: 'app/',
|
||||
value: 'app',
|
||||
children: [{
|
||||
label: 'composables/',
|
||||
value: 'composables',
|
||||
children: [
|
||||
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
|
||||
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'components/',
|
||||
value: 'components',
|
||||
children: [
|
||||
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
|
||||
]
|
||||
}]
|
||||
},
|
||||
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
|
||||
]
|
||||
|
||||
const expanded = ref(['app', 'composables'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTree v-model:expanded="expanded" :items="items" />
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeItem } from '@nuxt/ui'
|
||||
|
||||
const items: TreeItem[] = [
|
||||
{
|
||||
label: 'app/',
|
||||
defaultExpanded: true,
|
||||
children: [{
|
||||
label: 'composables/',
|
||||
children: [
|
||||
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
|
||||
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'components/',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
|
||||
]
|
||||
}]
|
||||
},
|
||||
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
|
||||
]
|
||||
|
||||
const value = ref(items[items.length - 1])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTree v-model="value" :items="items" />
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeItem } from '@nuxt/ui'
|
||||
|
||||
const items: TreeItem[] = [
|
||||
{
|
||||
label: 'app/',
|
||||
defaultExpanded: true,
|
||||
onSelect: (e: Event) => {
|
||||
e.preventDefault()
|
||||
},
|
||||
children: [{
|
||||
label: 'composables/',
|
||||
children: [
|
||||
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
|
||||
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'components/',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
|
||||
]
|
||||
}]
|
||||
},
|
||||
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTree :items="items" />
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeItem } from '@nuxt/ui'
|
||||
|
||||
const items: TreeItem[] = [
|
||||
{
|
||||
label: 'app/',
|
||||
defaultExpanded: true,
|
||||
onToggle: (e: Event) => {
|
||||
e.preventDefault()
|
||||
},
|
||||
children: [{
|
||||
label: 'composables/',
|
||||
children: [
|
||||
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
|
||||
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'components/',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
|
||||
]
|
||||
}]
|
||||
},
|
||||
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTree :items="items" />
|
||||
</template>
|
||||
139
docs/app/components/home/HomeContributors.vue
Normal file
139
docs/app/components/home/HomeContributors.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
contributors?: {
|
||||
username: string
|
||||
}[]
|
||||
level?: number
|
||||
max?: number
|
||||
paused?: boolean
|
||||
}>(), {
|
||||
level: 0,
|
||||
max: 4,
|
||||
paused: false
|
||||
})
|
||||
|
||||
const contributors = computed(() => props.contributors?.slice(0, 5) ?? [])
|
||||
|
||||
const el = ref(null)
|
||||
const { width } = useElementSize(el)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="isolate rounded-full relative circle w-full aspect-[1/1] p-8 sm:p-12 md:p-14 lg:p-10 xl:p-16 before:absolute before:inset-px before:bg-(--ui-bg) before:rounded-full z-(--level)"
|
||||
:class="{ 'animation-paused': paused }"
|
||||
:style="{
|
||||
'--duration': `${((level + 1) * 8)}s`,
|
||||
'--level': level + 1
|
||||
}"
|
||||
>
|
||||
<HomeContributors
|
||||
v-if="(level + 1) < max"
|
||||
:max="max"
|
||||
:level="level + 1"
|
||||
:contributors="props.contributors?.slice(5) ?? []"
|
||||
:paused="paused"
|
||||
/>
|
||||
|
||||
<div
|
||||
ref="el"
|
||||
class="avatars absolute inset-0 grid"
|
||||
:style="{
|
||||
'--total': contributors.length,
|
||||
'--offset': `${width / 2}px`
|
||||
}"
|
||||
>
|
||||
<UTooltip
|
||||
v-for="(contributor, index) in contributors"
|
||||
:key="contributor.username"
|
||||
:text="contributor.username"
|
||||
:delay-duration="0"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="`https://github.com/${contributor.username}`"
|
||||
:aria-label="contributor.username"
|
||||
target="_blank"
|
||||
class="avatar flex absolute top-1/2 left-1/2"
|
||||
tabindex="-1"
|
||||
:style="{
|
||||
'--index': index + 1
|
||||
}"
|
||||
>
|
||||
<img
|
||||
width="56"
|
||||
height="56"
|
||||
:src="`https://ipx.nuxt.com/s_56x56/gh_avatar/${contributor.username}`"
|
||||
:srcset="`https://ipx.nuxt.com/s_112x112/gh_avatar/${contributor.username} 2x`"
|
||||
:alt="contributor.username"
|
||||
class="ring-2 ring-(--ui-border) lg:hover:ring-(--ui-border-inverted) transition rounded-full size-7"
|
||||
loading="lazy"
|
||||
>
|
||||
</NuxtLink>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.circle:after {
|
||||
--start: 0deg;
|
||||
--end: 360deg;
|
||||
--border-color: var(--ui-border);
|
||||
--highlight-color: var(--ui-color-neutral-400);
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: -1px;
|
||||
opacity: 1;
|
||||
border-radius: 9999px;
|
||||
z-index: -1;
|
||||
background: var(--border-color);
|
||||
|
||||
@supports (background: paint(houdini)) {
|
||||
background: linear-gradient(var(--angle), var(--border-color), var(--border-color), var(--border-color), var(--border-color), var(--highlight-color));
|
||||
animation: var(--duration) rotate linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .circle:after {
|
||||
--highlight-color: var(--color-white);
|
||||
}
|
||||
|
||||
.animation-paused.circle:after,
|
||||
.animation-paused .avatars {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.avatars {
|
||||
--start: calc(var(--level) * 36deg);
|
||||
--end: calc(360deg + (var(--level) * 36deg));
|
||||
transform: rotate(var(--angle));
|
||||
animation: calc(var(--duration) + 60s) rotate linear infinite;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
--deg: calc(var(--index) * (360deg / var(--total)));
|
||||
--transformX: calc(cos(var(--deg)) * var(--offset));
|
||||
--transformY: calc(sin(var(--deg)) * var(--offset));
|
||||
transform: translate(calc(-50% + var(--transformX)), calc(-50% + var(--transformY))) rotate(calc(360deg - var(--angle)));
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
--angle: var(--start);
|
||||
}
|
||||
to {
|
||||
--angle: var(--end);
|
||||
}
|
||||
}
|
||||
|
||||
@property --angle {
|
||||
syntax: '<angle>';
|
||||
initial-value: 0deg;
|
||||
inherits: true;
|
||||
}
|
||||
</style>
|
||||
43
docs/app/components/og-image/OgImageComponent.vue
Normal file
43
docs/app/components/og-image/OgImageComponent.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
title: string
|
||||
description: string
|
||||
component: string
|
||||
module: string
|
||||
}>(), {
|
||||
module: ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white w-full h-full flex flex-col">
|
||||
<div class="absolute top-0 bottom-0 left-26 w-[2px] bg-slate-200" />
|
||||
<div class="absolute top-0 bottom-0 right-26 w-[2px] bg-slate-200" />
|
||||
<div class="absolute top-12 inset-x-0 h-[2px] bg-slate-200" />
|
||||
<svg
|
||||
class="absolute top-[24px] left-33 bg-white p-1 h-11 w-11"
|
||||
viewBox="0 0 512 512"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M281.44 397.667H438.32C443.326 397.667 448.118 395.908 452.453 393.427C456.789 390.946 461.258 387.831 463.76 383.533C466.262 379.236 468.002 374.36 468 369.399C467.998 364.437 466.266 359.563 463.76 355.268L357.76 172.947C355.258 168.65 352.201 165.534 347.867 163.053C343.532 160.573 337.325 158.813 332.32 158.813C327.315 158.813 322.521 160.573 318.187 163.053C313.852 165.534 310.795 168.65 308.293 172.947L281.44 219.587L227.733 129.13C225.229 124.834 222.176 120.307 217.84 117.827C213.504 115.346 208.713 115 203.707 115C198.701 115 193.909 115.346 189.573 117.827C185.238 120.307 180.771 124.834 178.267 129.13L46.8267 355.268C44.3208 359.563 44.0022 364.437 44 369.399C43.9978 374.36 44.3246 379.235 46.8267 383.533C49.3288 387.83 53.7979 390.946 58.1333 393.427C62.4688 395.908 67.2603 397.667 72.2667 397.667H171.2C210.401 397.667 238.934 380.082 258.827 346.787L306.88 263.4L332.32 219.587L410.053 352.44H306.88L281.44 397.667ZM169.787 352.44H100.533L203.707 174.36L256 263.4L221.361 323.784C208.151 345.387 193.089 352.44 169.787 352.44Z" fill="#00DC82" />
|
||||
</svg>
|
||||
<div class="w-full border-b-2 border-slate-200">
|
||||
<div class="mx-34 mt-16">
|
||||
<h1 class="text-4xl font-semibold mb-0 flex gap-1">
|
||||
<span>{{ title }}</span>
|
||||
<sup v-if="module === 'ui-pro'" class="text-base font-semibold align-top text-[#00C16A]">PRO</sup>
|
||||
</h1>
|
||||
<p class="text-2xl text-slate-500 truncate line-clamp-1 mt-2 mb-8">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-[106px] bg-slate-50 text-center">
|
||||
<img
|
||||
:src="`/components/light/${component}.png`"
|
||||
class="mx-auto h-[400px] object-contain"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,190 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
headline: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
withDefaults(defineProps<{
|
||||
title: string
|
||||
description: string
|
||||
headline: string
|
||||
framework: string
|
||||
module: string
|
||||
}>(), {
|
||||
framework: 'nuxt',
|
||||
module: ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col justify-center bg-slate-900">
|
||||
<svg
|
||||
class="absolute right-0 top-0"
|
||||
width="629"
|
||||
height="593"
|
||||
viewBox="0 0 629 593"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g filter="url(#filter0_f_199_94966)">
|
||||
<path d="M628.5 -578L639.334 -94.4223L806.598 -548.281L659.827 -87.387L965.396 -462.344L676.925 -74.0787L1087.69 -329.501L688.776 -55.9396L1160.22 -164.149L694.095 -34.9354L1175.13 15.7948L692.306 -13.3422L1130.8 190.83L683.602 6.50012L1032.04 341.989L668.927 22.4412L889.557 452.891L649.872 32.7537L718.78 511.519L628.5 36.32L538.22 511.519L607.128 32.7537L367.443 452.891L588.073 22.4412L224.955 341.989L573.398 6.50012L126.198 190.83L564.694 -13.3422L81.8734 15.7948L562.905 -34.9354L96.7839 -164.149L568.224 -55.9396L169.314 -329.501L580.075 -74.0787L291.604 -462.344L597.173 -87.387L450.402 -548.281L617.666 -94.4223L628.5 -578Z" fill="#00DC82" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_f_199_94966"
|
||||
x="0.873535"
|
||||
y="-659"
|
||||
width="1255.25"
|
||||
height="1251.52"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur stdDeviation="40.5" result="effect1_foregroundBlur_199_94966" />
|
||||
</filter>
|
||||
</defs>
|
||||
<div class="bg-white w-full h-full flex flex-col">
|
||||
<div class="absolute top-0 bottom-0 left-26 w-[2px] bg-slate-200" />
|
||||
<div class="absolute top-0 bottom-0 right-26 w-[2px] bg-slate-200" />
|
||||
<div class="absolute top-12 inset-x-0 h-[2px] bg-slate-200" />
|
||||
<div class="absolute bottom-26 inset-x-26 h-[2px] bg-slate-200" />
|
||||
<div class="absolute bottom-12 inset-x-0 h-[2px] bg-slate-200" />
|
||||
<svg class="absolute bottom-16 left-34 w-[133px] h-[26px]" viewBox="0 0 1020 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M377 200C379.16 200 381 198.209 381 196V103C381 103 386 112 395 127L434 194C435.785 197.74 439.744 200 443 200H470V50H443C441.202 50 439 51.4941 439 54V148L421 116L385 55C383.248 51.8912 379.479 50 376 50H350V200H377Z" fill="currentColor" />
|
||||
<path d="M726 92H739C742.314 92 745 89.3137 745 86V60H773V92H800V116H773V159C773 169.5 778.057 174 787 174H800V200H783C759.948 200 745 185.071 745 160V116H726V92Z" fill="currentColor" />
|
||||
<path d="M591 92V154C591 168.004 585.742 179.809 578 188C570.258 196.191 559.566 200 545 200C530.434 200 518.742 196.191 511 188C503.389 179.809 498 168.004 498 154V92H514C517.412 92 520.769 92.622 523 95C525.231 97.2459 526 98.5652 526 102V154C526 162.059 526.457 167.037 530 171C533.543 174.831 537.914 176 545 176C552.217 176 555.457 174.831 559 171C562.543 167.037 563 162.059 563 154V102C563 98.5652 563.769 96.378 566 94C567.96 91.9107 570.028 91.9599 573 92C573.411 92.0055 574.586 92 575 92H591Z" fill="currentColor" />
|
||||
<path d="M676 144L710 92H684C680.723 92 677.812 93.1758 676 96L660 120L645 97C643.188 94.1758 639.277 92 636 92H611L645 143L608 200H634C637.25 200 640.182 196.787 642 194L660 167L679 195C680.818 197.787 683.75 200 687 200H713L676 144Z" fill="currentColor" />
|
||||
<path d="M168 200H279C282.542 200 285.932 198.756 289 197C292.068 195.244 295.23 193.041 297 190C298.77 186.959 300.002 183.51 300 179.999C299.998 176.488 298.773 173.04 297 170.001L222 41C220.23 37.96 218.067 35.7552 215 34C211.933 32.2448 207.542 31 204 31C200.458 31 197.067 32.2448 194 34C190.933 35.7552 188.77 37.96 187 41L168 74L130 9.99764C128.228 6.95784 126.068 3.75491 123 2C119.932 0.245087 116.542 0 113 0C109.458 0 106.068 0.245087 103 2C99.9323 3.75491 96.7717 6.95784 95 9.99764L2 170.001C0.226979 173.04 0.00154312 176.488 1.90993e-06 179.999C-0.0015393 183.51 0.229648 186.959 2 190C3.77035 193.04 6.93245 195.244 10 197C13.0675 198.756 16.4578 200 20 200H90C117.737 200 137.925 187.558 152 164L186 105L204 74L259 168H186L168 200ZM89 168H40L113 42L150 105L125.491 147.725C116.144 163.01 105.488 168 89 168Z" fill="#00DC82" />
|
||||
<path d="M958 60.0001H938C933.524 60.0001 929.926 59.9395 927 63C924.074 65.8905 925 67.5792 925 72V141C925 151.372 923.648 156.899 919 162C914.352 166.931 908.468 169 899 169C889.705 169 882.648 166.931 878 162C873.352 156.899 873 151.372 873 141V72.0001C873 67.5793 872.926 65.8906 870 63.0001C867.074 59.9396 863.476 60.0001 859 60.0001H840V141C840 159.023 845.016 173.458 855 184C865.156 194.542 879.893 200 899 200C918.107 200 932.844 194.542 943 184C953.156 173.458 958 159.023 958 141V60.0001Z" fill="#00DC82" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1000 60.0233L1020 60V77L1020 128V156.007L1020 181L1020 189.004C1020 192.938 1019.98 194.429 1017 197.001C1014.02 199.725 1009.56 200 1005 200H986.001V181.006L986 130.012V70.0215C986 66.1576 986.016 64.5494 989 62.023C991.819 59.6358 995.437 60.0233 1000 60.0233Z" fill="#00DC82" />
|
||||
</svg>
|
||||
|
||||
<div class="w-[700px] pl-[100px]">
|
||||
<p v-if="headline" class="uppercase text-[24px] text-[#00DC82] mb-4 font-semibold">
|
||||
<div class="mx-26 mt-24 border-y-2 border-slate-200 h-14 flex flex-row items-center">
|
||||
<div class="h-full flex items-center border-r-2 border-slate-200 text-[#00C16A] px-6">
|
||||
<svg
|
||||
v-if="framework === 'nuxt'"
|
||||
class="h-[40px] w-[40px] mt-[10px]"
|
||||
viewBox="0 0 512 512"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M281.44 397.667H438.32C443.326 397.667 448.118 395.908 452.453 393.427C456.789 390.946 461.258 387.831 463.76 383.533C466.262 379.236 468.002 374.36 468 369.399C467.998 364.437 466.266 359.563 463.76 355.268L357.76 172.947C355.258 168.65 352.201 165.534 347.867 163.053C343.532 160.573 337.325 158.813 332.32 158.813C327.315 158.813 322.521 160.573 318.187 163.053C313.852 165.534 310.795 168.65 308.293 172.947L281.44 219.587L227.733 129.13C225.229 124.834 222.176 120.307 217.84 117.827C213.504 115.346 208.713 115 203.707 115C198.701 115 193.909 115.346 189.573 117.827C185.238 120.307 180.771 124.834 178.267 129.13L46.8267 355.268C44.3208 359.563 44.0022 364.437 44 369.399C43.9978 374.36 44.3246 379.235 46.8267 383.533C49.3288 387.83 53.7979 390.946 58.1333 393.427C62.4688 395.908 67.2603 397.667 72.2667 397.667H171.2C210.401 397.667 238.934 380.082 258.827 346.787L306.88 263.4L332.32 219.587L410.053 352.44H306.88L281.44 397.667ZM169.787 352.44H100.533L203.707 174.36L256 263.4L221.361 323.784C208.151 345.387 193.089 352.44 169.787 352.44Z" fill="#00DC82" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="w-[34px] h-[30px] mt-6"
|
||||
viewBox="0 0 261.76 226.69"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883" /></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e" /></g></g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="h-full uppercase flex items-center border-r-2 border-slate-200 text-[#00C16A] text-[20px] font-semibold px-6 pt-1">
|
||||
{{ headline }}
|
||||
</p>
|
||||
<h1 class="m-0 text-[75px] font-semibold mb-2 text-white flex items-center">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-34 mt-12 h-[240px] flex flex-col justify-center">
|
||||
<h1 class="text-5xl font-semibold mb-0 flex gap-1">
|
||||
<span>{{ title }}</span>
|
||||
<sup v-if="module === 'ui-pro'" class="text-base font-semibold align-top text-[#00C16A]">PRO</sup>
|
||||
</h1>
|
||||
<p v-if="description" class="text-[32px] text-[#94a3b8] leading-tight text-balance">
|
||||
{{ description.slice(0, 200) }}
|
||||
<p class="text-3xl text-slate-500 line-clamp-2">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<svg
|
||||
class="absolute top-[160px] right-[90px]"
|
||||
width="340"
|
||||
height="340"
|
||||
viewBox="0 0 340 340"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M86.6286 103.106C88.2099 94.7477 94.7477 88.2099 103.106 86.6286L104.427 86.3788C146.343 78.4485 189.386 78.5576 231.262 86.7002L232.272 86.8967C239.615 88.3244 245.4 93.99 246.981 101.301L247.277 102.671C256.565 145.63 256.438 190.092 246.903 232.997C245.361 239.939 239.939 245.361 232.997 246.903C190.092 256.438 145.63 256.565 102.671 247.277L101.301 246.981C93.99 245.4 88.3244 239.615 86.8967 232.272L86.7002 231.262C78.5576 189.386 78.4485 146.343 86.3788 104.426L86.6286 103.106Z" fill="url(#paint0_linear_199_94959)" />
|
||||
<path d="M86.6286 103.106C88.2099 94.7477 94.7477 88.2099 103.106 86.6286L104.427 86.3788C146.343 78.4485 189.386 78.5576 231.262 86.7002L232.272 86.8967C239.615 88.3244 245.4 93.99 246.981 101.301L247.277 102.671C256.565 145.63 256.438 190.092 246.903 232.997C245.361 239.939 239.939 245.361 232.997 246.903C190.092 256.438 145.63 256.565 102.671 247.277L101.301 246.981C93.99 245.4 88.3244 239.615 86.8967 232.272L86.7002 231.262C78.5576 189.386 78.4485 146.343 86.3788 104.426L86.6286 103.106Z" fill="url(#paint1_radial_199_94959)" fill-opacity="0.06" />
|
||||
<path d="M103.028 86.2151C94.4994 87.8286 87.8286 94.4994 86.2151 103.028L85.9653 104.348C78.0252 146.318 78.1344 189.414 86.2872 231.342L86.4836 232.353C87.9434 239.86 93.7366 245.776 101.212 247.392L102.582 247.688C145.601 256.989 190.124 256.862 233.089 247.314C240.19 245.736 245.736 240.19 247.314 233.089C256.862 190.124 256.989 145.601 247.688 102.582L247.392 101.212C245.776 93.7366 239.86 87.9434 232.353 86.4836L231.342 86.2872C189.414 78.1344 146.318 78.0252 104.348 85.9653L103.028 86.2151Z" stroke="url(#paint2_linear_199_94959)" stroke-opacity="0.2" stroke-width="0.841584" />
|
||||
<path d="M86.5693 66.0418C76.1888 68.0728 68.0728 76.1888 66.0418 86.5693L65.7807 87.9041C55.4495 140.708 55.5631 195.024 66.1151 247.784L66.3095 248.756C68.1871 258.144 75.4486 265.527 84.8039 267.561L86.1767 267.86C140.001 279.561 195.755 279.425 249.525 267.477C258.483 265.486 265.485 258.489 267.475 249.53C279.429 195.737 279.558 139.989 267.852 86.1413L267.553 84.7671C265.524 75.4341 258.158 68.19 248.793 66.3169L247.784 66.1151C195.024 55.5631 140.708 55.4495 87.904 65.7807L86.5693 66.0418Z" stroke="url(#paint3_linear_199_94959)" stroke-opacity="0.6" stroke-width="1.7" />
|
||||
<path d="M66.1749 44.2983C55.2117 46.6476 46.6475 55.2117 44.2983 66.1749L43.1523 71.5228C29.5422 135.037 29.5422 200.713 43.1523 264.227L44.425 270.166C46.7026 280.795 54.917 289.15 65.5055 291.608C132.857 307.243 203.127 307.189 270.477 291.554C280.924 289.129 289.129 280.924 291.554 270.477C307.189 203.127 307.243 132.857 291.608 65.5055C289.15 54.917 280.795 46.7026 270.166 44.425L264.227 43.1523C200.713 29.5422 135.037 29.5422 71.5228 43.1523L66.1749 44.2983Z" stroke="url(#paint4_linear_199_94959)" stroke-opacity="0.4" stroke-width="1.7" />
|
||||
<path d="M47.0949 24.9846C36.0029 27.3375 27.3375 36.0029 24.9846 47.0949L19.3193 73.8027C5.86738 137.219 5.96967 202.762 19.6195 266.137L25.0028 291.131C27.3518 302.037 35.8106 310.592 46.6894 313.064L67.2583 317.739C134.879 333.107 205.098 332.991 272.667 317.398L291.54 313.043C302.229 310.576 310.576 302.229 313.043 291.54L317.398 272.667C332.991 205.098 333.107 134.879 317.739 67.2583L313.064 46.6894C310.592 35.8106 302.037 27.3518 291.131 25.0028L266.137 19.6195C202.762 5.96966 137.219 5.86738 73.8027 19.3193L47.0949 24.9846Z" stroke="url(#paint5_linear_199_94959)" stroke-opacity="0.2" stroke-width="2.125" />
|
||||
<path d="M174.667 190.325H203.105C204.009 190.325 204.896 190.084 205.678 189.626C206.461 189.168 207.11 188.509 207.561 187.715C208.013 186.921 208.25 186.021 208.25 185.105C208.25 184.188 208.011 183.288 207.559 182.495L188.461 148.938C188.009 148.145 187.36 147.486 186.578 147.028C185.796 146.57 184.909 146.328 184.006 146.328C183.103 146.328 182.215 146.57 181.433 147.028C180.651 147.486 180.002 148.145 179.551 148.938L174.667 157.524L165.119 140.734C164.668 139.941 164.018 139.282 163.236 138.824C162.453 138.366 161.566 138.125 160.663 138.125C159.76 138.125 158.872 138.366 158.09 138.824C157.308 139.282 156.658 139.941 156.206 140.734L132.441 182.495C131.989 183.288 131.75 184.188 131.75 185.105C131.75 186.021 131.987 186.921 132.439 187.715C132.89 188.509 133.539 189.168 134.322 189.626C135.104 190.084 135.991 190.325 136.895 190.325H154.746C161.819 190.325 167.035 187.173 170.624 181.025L179.337 165.717L184.004 157.524L198.011 182.133H179.337L174.667 190.325ZM154.455 182.124L141.997 182.121L160.671 149.312L169.989 165.717L163.75 176.681C161.367 180.671 158.659 182.124 154.455 182.124Z" fill="#00DC82" />
|
||||
<path d="M176.761 189.109H203.105H203.106C203.792 189.109 204.467 188.926 205.064 188.576C205.66 188.227 206.158 187.723 206.504 187.113L207.561 187.715L206.504 187.113C206.851 186.504 207.034 185.811 207.034 185.105C207.033 184.399 206.85 183.707 206.502 183.098L206.502 183.097L187.404 149.54L187.404 149.54C187.057 148.93 186.56 148.427 185.963 148.077C185.367 147.728 184.692 147.545 184.006 147.545C183.32 147.545 182.645 147.728 182.048 148.077C181.452 148.427 180.954 148.93 180.608 149.539C180.608 149.539 180.608 149.54 180.608 149.54L175.725 158.126L174.667 159.985L173.61 158.125L164.062 141.336L176.761 189.109ZM176.761 189.109L180.044 183.349H198.011H200.103L199.069 181.531L185.061 156.922L184.005 155.066L182.947 156.922L178.28 165.115L178.28 165.115L169.57 180.417C166.187 186.208 161.362 189.109 154.746 189.109H136.895H136.894C136.208 189.109 135.533 188.926 134.936 188.576C134.34 188.227 133.842 187.723 133.496 187.113L132.44 187.714L133.496 187.113C133.149 186.504 132.966 185.811 132.966 185.105C132.967 184.399 133.15 183.707 133.498 183.098L133.498 183.097L157.263 141.336C157.263 141.336 157.263 141.336 157.263 141.336C157.61 140.727 158.108 140.223 158.705 139.874C159.301 139.525 159.976 139.341 160.663 139.341C161.349 139.341 162.025 139.525 162.621 139.874L163.236 138.824L162.621 139.874C163.218 140.223 163.715 140.727 164.062 141.336L176.761 189.109ZM154.454 183.34H154.455C156.699 183.34 158.653 182.952 160.391 181.953C162.126 180.957 163.533 179.417 164.794 177.305L164.801 177.294L164.807 177.283L171.046 166.318L171.388 165.717L171.047 165.116L161.729 148.711L160.672 146.851L159.614 148.71L140.94 181.52L139.905 183.337L141.997 183.338L154.454 183.34Z" stroke="url(#paint6_linear_199_94959)" stroke-opacity="0.2" stroke-width="2.43271" />
|
||||
<g filter="url(#filter0_f_199_94959)">
|
||||
<path d="M174.667 190.325H203.105C204.009 190.325 204.896 190.084 205.678 189.626C206.461 189.168 207.11 188.509 207.561 187.715C208.013 186.921 208.25 186.021 208.25 185.105C208.25 184.188 208.011 183.288 207.559 182.495L188.461 148.938C188.009 148.145 187.36 147.486 186.578 147.028C185.796 146.57 184.909 146.328 184.006 146.328C183.103 146.328 182.215 146.57 181.433 147.028C180.651 147.486 180.002 148.145 179.551 148.938L174.667 157.524L165.119 140.734C164.668 139.941 164.018 139.282 163.236 138.824C162.453 138.366 161.566 138.125 160.663 138.125C159.76 138.125 158.872 138.366 158.09 138.824C157.308 139.282 156.658 139.941 156.206 140.734L132.441 182.495C131.989 183.288 131.75 184.188 131.75 185.105C131.75 186.021 131.987 186.921 132.439 187.715C132.89 188.509 133.539 189.168 134.322 189.626C135.104 190.084 135.991 190.325 136.895 190.325H154.746C161.819 190.325 167.035 187.173 170.624 181.025L179.337 165.717L184.004 157.524L198.011 182.133H179.337L174.667 190.325ZM154.455 182.124L141.997 182.121L160.671 149.312L169.989 165.717L163.75 176.681C161.367 180.671 158.659 182.124 154.455 182.124Z" fill="#00DC82" />
|
||||
<path d="M176.761 189.109H203.105H203.106C203.792 189.109 204.467 188.926 205.064 188.576C205.66 188.227 206.158 187.723 206.504 187.113L207.561 187.715L206.504 187.113C206.851 186.504 207.034 185.811 207.034 185.105C207.033 184.399 206.85 183.707 206.502 183.098L206.502 183.097L187.404 149.54L187.404 149.54C187.057 148.93 186.56 148.427 185.963 148.077C185.367 147.728 184.692 147.545 184.006 147.545C183.32 147.545 182.645 147.728 182.048 148.077C181.452 148.427 180.954 148.93 180.608 149.539C180.608 149.539 180.608 149.54 180.608 149.54L175.725 158.126L174.667 159.985L173.61 158.125L164.062 141.336L176.761 189.109ZM176.761 189.109L180.044 183.349H198.011H200.103L199.069 181.531L185.061 156.922L184.005 155.066L182.947 156.922L178.28 165.115L178.28 165.115L169.57 180.417C166.187 186.208 161.362 189.109 154.746 189.109H136.895H136.894C136.208 189.109 135.533 188.926 134.936 188.576C134.34 188.227 133.842 187.723 133.496 187.113L132.44 187.714L133.496 187.113C133.149 186.504 132.966 185.811 132.966 185.105C132.967 184.399 133.15 183.707 133.498 183.098L133.498 183.097L157.263 141.336C157.263 141.336 157.263 141.336 157.263 141.336C157.61 140.727 158.108 140.223 158.705 139.874C159.301 139.525 159.976 139.341 160.663 139.341C161.349 139.341 162.025 139.525 162.621 139.874L163.236 138.824L162.621 139.874C163.218 140.223 163.715 140.727 164.062 141.336L176.761 189.109ZM154.454 183.34H154.455C156.699 183.34 158.653 182.952 160.391 181.953C162.126 180.957 163.533 179.417 164.794 177.305L164.801 177.294L164.807 177.283L171.046 166.318L171.388 165.717L171.047 165.116L161.729 148.711L160.672 146.851L159.614 148.71L140.94 181.52L139.905 183.337L141.997 183.338L154.454 183.34Z" stroke="url(#paint7_linear_199_94959)" stroke-opacity="0.2" stroke-width="2.43271" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_f_199_94959"
|
||||
x="124.176"
|
||||
y="130.551"
|
||||
width="91.6485"
|
||||
height="67.3485"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur stdDeviation="3.78713" result="effect1_foregroundBlur_199_94959" />
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_199_94959"
|
||||
x1="167.875"
|
||||
y1="74.375"
|
||||
x2="88"
|
||||
y2="261"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#0F172A" />
|
||||
<stop offset="1" stop-color="#0F172A" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="paint1_radial_199_94959"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(167.875 167.875) rotate(-90) scale(100.596 107.502)"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_199_94959"
|
||||
x1="247.183"
|
||||
y1="96.4978"
|
||||
x2="194.172"
|
||||
y2="229.234"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_199_94959"
|
||||
x1="267.01"
|
||||
y1="78.6537"
|
||||
x2="200.746"
|
||||
y2="244.574"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#00DC82" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_199_94959"
|
||||
x1="292.405"
|
||||
y1="57.8159"
|
||||
x2="209.877"
|
||||
y2="264.463"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#00DC82" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint5_linear_199_94959"
|
||||
x1="314.196"
|
||||
y1="40.2232"
|
||||
x2="217.813"
|
||||
y2="281.562"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#00DC82" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint6_linear_199_94959"
|
||||
x1="202.444"
|
||||
y1="144.3"
|
||||
x2="191.546"
|
||||
y2="184.293"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint7_linear_199_94959"
|
||||
x1="202.444"
|
||||
y1="144.3"
|
||||
x2="191.546"
|
||||
y2="184.293"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,6 +37,10 @@ export const useContentNavigation = (navigation: Ref<ContentNavigationItem[] | u
|
||||
return {
|
||||
...item,
|
||||
children: item.children?.filter((child: any) => {
|
||||
if (child.path.startsWith('/components')) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (child.framework && child.framework !== framework.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,12 +5,49 @@ export function useLinks() {
|
||||
label: 'Docs',
|
||||
icon: 'i-lucide-square-play',
|
||||
to: '/getting-started',
|
||||
active: route.path.startsWith('/getting-started')
|
||||
active: route.path.startsWith('/getting-started') || route.path.startsWith('/composables/') || (route.path.startsWith('/components/') && route.name !== 'components')
|
||||
}, {
|
||||
label: 'Components',
|
||||
icon: 'i-lucide-square-code',
|
||||
to: '/components',
|
||||
active: route.path.startsWith('/components')
|
||||
active: route.path === '/components',
|
||||
children: [{
|
||||
label: 'Element',
|
||||
to: '/components#element',
|
||||
description: 'Button, badge, icon, alert, and small UI elements.',
|
||||
icon: 'i-lucide-mouse-pointer',
|
||||
active: route.fullPath === '/components#element'
|
||||
}, {
|
||||
label: 'Form',
|
||||
to: '/components#form',
|
||||
description: 'Input, select, checkbox, radio and form validation.',
|
||||
icon: 'i-lucide-text-cursor-input',
|
||||
active: route.fullPath === '/components#form'
|
||||
}, {
|
||||
label: 'Data',
|
||||
to: '/components#data',
|
||||
description: 'Table, list, card, carousel and visualization elements.',
|
||||
icon: 'i-lucide-table',
|
||||
active: route.fullPath === '/components#data'
|
||||
}, {
|
||||
label: 'Navigation',
|
||||
to: '/components#navigation',
|
||||
description: 'Menu, breadcrumb, pagination and navbar.',
|
||||
icon: 'i-lucide-link',
|
||||
active: route.fullPath === '/components#navigation'
|
||||
}, {
|
||||
label: 'Overlay',
|
||||
to: '/components#overlay',
|
||||
description: 'Modal, tooltip, dialog and popover.',
|
||||
icon: 'i-lucide-layers',
|
||||
active: route.fullPath === '/components#overlay'
|
||||
}, {
|
||||
label: 'Layout',
|
||||
to: '/components#layout',
|
||||
description: 'Container, grid, divider and responsive layout.',
|
||||
icon: 'i-lucide-layout',
|
||||
active: route.fullPath === '/components#layout'
|
||||
}]
|
||||
}, {
|
||||
label: 'Pro',
|
||||
icon: 'i-lucide-panels-top-left',
|
||||
@@ -41,12 +78,35 @@ export function useLinks() {
|
||||
}]
|
||||
}, {
|
||||
label: 'Figma',
|
||||
icon: 'i-lucide-figma',
|
||||
icon: 'i-simple-icons-figma',
|
||||
to: '/figma'
|
||||
}, {
|
||||
label: 'Roadmap',
|
||||
icon: 'i-lucide-map',
|
||||
to: '/roadmap'
|
||||
label: 'Community',
|
||||
icon: 'i-lucide-users',
|
||||
children: [{
|
||||
label: 'Roadmap',
|
||||
description: 'Track our development progress in real-time.',
|
||||
icon: 'i-lucide-map',
|
||||
to: '/roadmap'
|
||||
}, {
|
||||
label: 'Devtools Integration',
|
||||
description: 'Integrate Nuxt UI with Nuxt Devtools with Compodium.',
|
||||
icon: 'i-lucide-code',
|
||||
to: 'https://github.com/romhml/compodium',
|
||||
target: '_blank'
|
||||
}, {
|
||||
label: 'Raycast Extension',
|
||||
description: 'Access Nuxt UI components without leaving your editor.',
|
||||
icon: 'i-simple-icons-raycast',
|
||||
to: 'https://www.raycast.com/HugoRCD/nuxt-ui',
|
||||
target: '_blank'
|
||||
}, {
|
||||
label: 'Figma to Code',
|
||||
description: 'Convert Figma designs to Nuxt UI code.',
|
||||
icon: 'i-simple-icons-figma',
|
||||
to: 'https://github.com/Justineo/tempad-dev-plugin-nuxt-ui',
|
||||
target: '_blank'
|
||||
}]
|
||||
}, {
|
||||
label: 'Releases',
|
||||
icon: 'i-lucide-rocket',
|
||||
|
||||
@@ -9,7 +9,6 @@ export function useSharedData() {
|
||||
label: 'Vue',
|
||||
icon: 'i-simple-icons-vuedotjs',
|
||||
value: 'vue',
|
||||
disabled: module.value === 'ui-pro',
|
||||
onSelect: () => {
|
||||
if (module.value === 'ui-pro') {
|
||||
return
|
||||
@@ -29,7 +28,6 @@ export function useSharedData() {
|
||||
label: 'UI Pro',
|
||||
icon: 'i-lucide-panels-top-left',
|
||||
value: 'ui-pro',
|
||||
disabled: framework.value === 'vue',
|
||||
onSelect: () => {
|
||||
if (framework.value === 'vue') {
|
||||
return
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
182
docs/app/pages/.index.yml
Normal file
182
docs/app/pages/.index.yml
Normal file
@@ -0,0 +1,182 @@
|
||||
title: The Intuitive Vue UI Library
|
||||
description: Create beautiful, responsive & accessible web apps quickly with Vue or Nuxt. Nuxt UI is an open-source UI library of 50+ customizable components built with Tailwind CSS and Reka UI.
|
||||
hero:
|
||||
title: The Intuitive Vue UI Library
|
||||
description: Create beautiful, responsive & accessible web apps quickly with Vue or Nuxt. Nuxt UI is an open-source UI library of 50+ customizable components built with Tailwind CSS and Reka UI.
|
||||
links:
|
||||
- label: Get started
|
||||
to: /getting-started/installation/nuxt
|
||||
class: 'ui-only nuxt-only'
|
||||
- label: Get started
|
||||
to: /getting-started/installation/vue
|
||||
class: 'ui-only vue-only'
|
||||
- label: Get started
|
||||
to: /getting-started/installation/pro/nuxt
|
||||
class: 'ui-pro-only nuxt-only'
|
||||
- label: Get started
|
||||
to: /getting-started/installation/pro/vue
|
||||
class: 'ui-pro-only vue-only'
|
||||
- label: Explore components
|
||||
to: /components
|
||||
variant: outline
|
||||
color: neutral
|
||||
trailingIcon: i-lucide-arrow-right
|
||||
features:
|
||||
- icon: i-logos-tailwindcss-icon
|
||||
title: Styled with Tailwind CSS v4
|
||||
description: Beautifully styled by default, overwrite any style you want.
|
||||
- icon: i-custom-reka-ui
|
||||
title: Accessible with Reka UI
|
||||
description: Robust accessibility out of the box.
|
||||
- icon: i-logos-typescript-icon
|
||||
title: Type-safe with TypeScript
|
||||
description: Auto-complete and type safety for all components.
|
||||
features:
|
||||
- title: Build for the modern web
|
||||
description: Powered by Tailwind CSS v4 and Reka UI for top performance and accessibility.
|
||||
icon: i-lucide-rocket
|
||||
to: /getting-started
|
||||
- title: Flexible design system
|
||||
description: Beautiful by default and easily customizable with design tokens to your brand.
|
||||
icon: i-lucide-swatch-book
|
||||
to: /getting-started/theme#design-system
|
||||
- title: Internationalization (i18n)
|
||||
description: Nuxt UI is translated into 30+ languages, works well with i18n and multi-directional support (LTR/RTL).
|
||||
icon: i-lucide-globe
|
||||
to: /getting-started/i18n/nuxt
|
||||
- title: Easy font customization
|
||||
description: Performance-optimized fonts with first-class @nuxt/fonts integration.
|
||||
icon: i-lucide-a-large-small
|
||||
to: /getting-started/fonts
|
||||
- title: Large icons sets
|
||||
description: Access to over 200,000 customizable icons from Iconify, seamlessly integrated with Iconify.
|
||||
icon: i-lucide-smile
|
||||
to: /getting-started/icons
|
||||
- title: Light & Dark
|
||||
description: Dark mode-ready components, seamless integration with @nuxtjs/color-mode.
|
||||
icon: i-lucide-sun-moon
|
||||
to: /getting-started/color-mode/nuxt
|
||||
design_system:
|
||||
title: Flexible design system
|
||||
description: Build your next project faster with Nuxt UI's comprehensive design system. Featuring semantic color aliases, comprehensive design tokens, and automatic light/dark mode support for accessible components out of the box.
|
||||
links:
|
||||
- label: Customize design system
|
||||
to: /getting-started/theme#design-system
|
||||
variant: outline
|
||||
color: neutral
|
||||
trailingIcon: i-lucide-arrow-right
|
||||
features:
|
||||
- title: Color aliases via AppConfig
|
||||
description: Configure 7 semantic color aliases (primary, secondary, success, info, warning, error, neutral) at runtime through AppConfig without rebuilding your application
|
||||
icon: i-lucide-palette
|
||||
- title: Comprehensive design tokens
|
||||
description: Extensive set of neutral palette tokens for text, backgrounds, and borders with automatic light/dark mode support via CSS variables like --ui-text, --ui-bg, --ui-border
|
||||
icon: i-lucide-component
|
||||
- title: Global style variables
|
||||
description: Customize global styling with --ui-radius for consistent border rounding and --ui-container for layout widths across your entire application
|
||||
icon: i-lucide-ruler
|
||||
code: |
|
||||
::code-group
|
||||
|
||||
```ts [app.config.ts]
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'indigo',
|
||||
secondary: 'pink',
|
||||
success: 'green',
|
||||
info: 'blue',
|
||||
warning: 'orange',
|
||||
error: 'red',
|
||||
neutral: 'zinc'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
```
|
||||
|
||||
```css [main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
|
||||
:root {
|
||||
--ui-radius: var(--radius-sm);
|
||||
--ui-container: 90rem;
|
||||
--ui-bg: var(--ui-color-neutral-50);
|
||||
--ui-text: var(--ui-color-neutral-900);
|
||||
}
|
||||
.dark {
|
||||
--ui-bg: var(--ui-color-neutral-950);
|
||||
--ui-border: var(--ui-color-neutral-900);
|
||||
}
|
||||
```
|
||||
|
||||
::
|
||||
component_customization:
|
||||
title: Powerful component customization
|
||||
description: Nuxt UI leverages [Tailwind Variants](https://www.tailwind-variants.org/) to provide a powerful, maintainable system for managing component styles and intelligently merging Tailwind CSS classes without conflicts.
|
||||
links:
|
||||
- label: Customize components
|
||||
to: /getting-started/theme#customize-theme
|
||||
variant: outline
|
||||
color: neutral
|
||||
trailingIcon: i-lucide-arrow-right
|
||||
features:
|
||||
- title: Powerful slot and variant system
|
||||
description: Customize component parts with slots and apply different styles based on props, creating consistent UI patterns with granular control over styling
|
||||
icon: i-lucide-layout-grid
|
||||
- title: Global theme with AppConfig
|
||||
description: Configure component styles project-wide with a centralized AppConfig that maintains consistency across your application without rebuilding
|
||||
icon: i-lucide-settings-2
|
||||
- title: Per-component customization
|
||||
description: Fine-tune individual components with the ui prop for slot-specific styling and class prop for root element overrides, providing maximum flexibility
|
||||
icon: i-lucide-component
|
||||
code: |
|
||||
::code-group
|
||||
|
||||
```ts [app.config.ts]
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
button: {
|
||||
slots: {
|
||||
base: 'group font-bold',
|
||||
trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
|
||||
},
|
||||
defaultVariants: {
|
||||
color: 'neutral',
|
||||
variant: 'subtle'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```vue [Collapsible.vue]
|
||||
<template>
|
||||
<UCollapsible>
|
||||
<UButton
|
||||
label="Open"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
:ui="{
|
||||
trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
|
||||
}"
|
||||
class="group font-bold"
|
||||
/>
|
||||
</UCollapsible>
|
||||
</template>
|
||||
```
|
||||
|
||||
::
|
||||
community:
|
||||
title: Nuxt UI open-source community
|
||||
description: Join our thriving community to contribute code, report issues, suggest features, or help with documentation. Every contribution makes Nuxt UI better for everyone.
|
||||
links:
|
||||
- label: Star on GitHub
|
||||
color: neutral
|
||||
variant: outline
|
||||
to: https://github.com/nuxt/ui
|
||||
target: _blank
|
||||
icon: i-lucide-star
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { kebabCase } from 'scule'
|
||||
import type { ContentNavigationItem } from '@nuxt/content'
|
||||
import { findPageBreadcrumb, mapContentNavigation } from '#ui-pro/utils/content'
|
||||
|
||||
@@ -9,7 +10,7 @@ definePageMeta({
|
||||
layout: 'docs'
|
||||
})
|
||||
|
||||
const { data: page } = await useAsyncData(route.path, () => queryCollection('content').path(route.path).first())
|
||||
const { data: page } = await useAsyncData(kebabCase(route.path), () => queryCollection('content').path(route.path).first())
|
||||
if (!page.value) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||
}
|
||||
@@ -24,7 +25,7 @@ watch(page, () => {
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
|
||||
const { data: surround } = await useAsyncData(`${kebabCase(route.path)}-surround`, () => {
|
||||
return queryCollectionItemSurroundings('content', route.path, {
|
||||
fields: ['description']
|
||||
}).orWhere(group => group.where('framework', '=', framework.value).where('framework', 'IS NULL'))
|
||||
@@ -72,9 +73,20 @@ useSeoMeta({
|
||||
ogDescription: page.value.description
|
||||
})
|
||||
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: breadcrumb.value.map(item => item.label).join(' > ')
|
||||
})
|
||||
if (route.path.startsWith('/components')) {
|
||||
defineOgImageComponent('OgImageComponent', {
|
||||
title: page.value.title,
|
||||
description: page.value.description,
|
||||
component: (route.params.slug as string[]).pop() as string,
|
||||
module: page.value.module
|
||||
})
|
||||
} else {
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: breadcrumb.value?.[breadcrumb.value.length - 1]?.label || 'Nuxt UI',
|
||||
framework: page.value?.framework,
|
||||
module: page.value.module
|
||||
})
|
||||
}
|
||||
|
||||
const communityLinks = computed(() => [{
|
||||
icon: 'i-lucide-file-pen',
|
||||
@@ -116,13 +128,17 @@ const communityLinks = computed(() => [{
|
||||
|
||||
<template>
|
||||
<UPage v-if="page">
|
||||
<UPageHeader :title="page.title">
|
||||
<UPageHeader>
|
||||
<template #headline>
|
||||
<UBreadcrumb :items="breadcrumb" />
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
{{ page.title }}<sup v-if="page.module === 'ui-pro'" class="ml-1 text-xs align-super font-medium text-(--ui-primary)">PRO</sup>
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
<MDC v-if="page.description" :value="page.description" unwrap="p" />
|
||||
<MDC v-if="page.description" :value="page.description" unwrap="p" :cache-key="`${kebabCase(route.path)}-description`" />
|
||||
</template>
|
||||
|
||||
<template v-if="page.links?.length" #links>
|
||||
|
||||
178
docs/app/pages/components.vue
Normal file
178
docs/app/pages/components.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
import { joinURL } from 'ufo'
|
||||
|
||||
const { url } = useSiteConfig()
|
||||
|
||||
const title = 'Vue Components'
|
||||
const description = 'Explore 99+ customizable UI components for Vue and Nuxt built with Tailwind CSS and Reka UI.'
|
||||
|
||||
useSeoMeta({
|
||||
titleTemplate: `%s - Nuxt UI`,
|
||||
title,
|
||||
description,
|
||||
ogTitle: `${title} - Nuxt UI`,
|
||||
ogDescription: description,
|
||||
ogImage: joinURL(url, '/og-image.png')
|
||||
})
|
||||
|
||||
const { data: components } = await useAsyncData('all-components', () => {
|
||||
return queryCollection('content')
|
||||
.where('path', 'LIKE', '/components/%')
|
||||
.where('extension', '=', 'md')
|
||||
.select('path', 'title', 'description', 'category', 'module')
|
||||
.all()
|
||||
})
|
||||
|
||||
const componentsPerCategory = computed(() => {
|
||||
return components.value!.reduce((acc, component) => {
|
||||
acc[component.category!] = [...(acc[component.category!] || []), component]
|
||||
return acc
|
||||
}, {} as Record<string, any[]>)
|
||||
})
|
||||
|
||||
const categories = [{
|
||||
id: 'element',
|
||||
title: 'Element',
|
||||
description: 'Core UI building blocks like buttons, badges, icons, avatars, and other fundamental interface elements.'
|
||||
}, {
|
||||
id: 'form',
|
||||
title: 'Form',
|
||||
description: 'Interactive form elements including inputs, selects, checkboxes, radio buttons, and advanced form validation components.'
|
||||
}, {
|
||||
id: 'data',
|
||||
title: 'Data',
|
||||
description: 'Components for displaying and managing data, including tables, lists, cards, data grids, and visualization elements.'
|
||||
}, {
|
||||
id: 'navigation',
|
||||
title: 'Navigation',
|
||||
description: 'Components for user navigation and wayfinding, including menus, breadcrumbs, pagination, and navigation bars.'
|
||||
}, {
|
||||
id: 'overlay',
|
||||
title: 'Overlay',
|
||||
description: 'Floating UI elements like modals, dialogs, tooltips, popovers, and other components that overlay the main content.'
|
||||
}, {
|
||||
id: 'layout',
|
||||
title: 'Layout',
|
||||
description: 'Structural components for organizing content, including containers, grids, dividers, and responsive layout systems.'
|
||||
}]
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
onMounted(() => {
|
||||
const stickyElements = document.querySelectorAll('[data-track-sticky]') as NodeListOf<HTMLElement>
|
||||
watch(y, () => {
|
||||
stickyElements.forEach((el) => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const topComputed = Number.parseInt(window.getComputedStyle(el).top || '0', 10)
|
||||
if (rect.top <= topComputed) {
|
||||
el.dataset.stuck = ''
|
||||
} else {
|
||||
delete el.dataset.stuck
|
||||
}
|
||||
})
|
||||
}, { immediate: true })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UMain>
|
||||
<UPageHero
|
||||
description="Build your Vue or Nuxt application faster with Nuxt UI and Nuxt UI Pro components. Powered by Tailwind CSS and Reka UI, delivering responsive and customizable components."
|
||||
class="relative"
|
||||
orientation="vertical"
|
||||
:ui="{ title: 'text-balance', container: 'relative' }"
|
||||
>
|
||||
<template #top>
|
||||
<div class="absolute z-[-1] rounded-full bg-(--ui-primary) blur-[300px] size-60 sm:size-80 transform -translate-x-1/2 left-1/2 -translate-y-80" />
|
||||
</template>
|
||||
|
||||
<template #headline>
|
||||
<UButton
|
||||
to="https://tailwindcss.com"
|
||||
label="Made with Tailwind CSS v4"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
color="neutral"
|
||||
icon="i-logos-tailwindcss-icon"
|
||||
class="rounded-full"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
Build beautiful UI with <span class="text-(--ui-primary)">{{ components!.length }}+</span> powerful components
|
||||
</template>
|
||||
|
||||
<template #links>
|
||||
<UButton
|
||||
to="/getting-started/installation/vue"
|
||||
label="Start with Vue"
|
||||
icon="i-logos-vue"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xl"
|
||||
/>
|
||||
<UButton
|
||||
to="/getting-started/installation/nuxt"
|
||||
label="Start with Nuxt"
|
||||
icon="i-logos-nuxt-icon"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xl"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<StarsBg />
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
</UPageHero>
|
||||
|
||||
<div v-for="category in categories" :key="category.id">
|
||||
<div data-track-sticky class="group mb-4 sm:mb-6 lg:mb-8 sticky top-[calc(var(--ui-header-height)-1px)] bg-(--ui-bg)/75 backdrop-blur z-[1]">
|
||||
<div class="relative border-y border-(--ui-border) py-4 sm:not-group-[[data-stuck]]:py-6 lg:not-group-[[data-stuck]]:py-8 transition-all duration-300">
|
||||
<UContainer>
|
||||
<h2 class="relative text-pretty font-bold text-(--ui-text-highlighted) text-base sm:not-group-[[data-stuck]]:text-xl lg:not-group-[[data-stuck]]:text-2xl transition-all duration-300 ">
|
||||
<a :href="`#${category.id}`" class="group lg:not-group-[[data-stuck]]:ps-2 lg:not-group-[[data-stuck]]:-ms-2">
|
||||
<span class="absolute -ms-8 top-1 opacity-0 group-hover:opacity-100 group-focus:opacity-100 p-1 bg-(--ui-bg-elevated) hover:text-(--ui-primary) rounded-[calc(var(--ui-radius)*1.5)] hidden lg:not-group-[[data-stuck]]:flex text-(--ui-text-muted) transition">
|
||||
<UIcon name="i-lucide-hash" class="size-4 shrink-0" />
|
||||
</span>
|
||||
{{ category.title }}
|
||||
</a>
|
||||
</h2>
|
||||
<p class="text-pretty text-(--ui-text-muted) text-sm sm:not-group-[[data-stuck]]:text-base lg:not-group-[[data-stuck]]:text-lg mt-1 sm:not-group-[[data-stuck]]:mt-2 line-clamp-1 transition-all duration-300">
|
||||
{{ category.description }}
|
||||
</p>
|
||||
</UContainer>
|
||||
</div>
|
||||
</div>
|
||||
<UContainer>
|
||||
<UPageGrid :id="category.id" class="xl:grid-cols-4 gap-4 sm:gap-6 lg:gap-8 pb-24 scroll-mt-[calc(97px+var(--ui-header-height))] sm:scroll-mt-[calc(133px+var(--ui-header-height))] lg:scroll-mt-[calc(165px+var(--ui-header-height))]">
|
||||
<UPageCard
|
||||
v-for="(component, index) in componentsPerCategory[category.id]"
|
||||
:key="component.path"
|
||||
variant="naked"
|
||||
:title="component.title"
|
||||
:description="component.description"
|
||||
:to="component.path"
|
||||
:ui="{ wrapper: 'order-last', container: 'lg:flex' }"
|
||||
class="group"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<span>{{ component.title }}</span>
|
||||
<sup v-if="component.module === 'ui-pro'" class="text-[8px] font-medium text-(--ui-primary)">PRO</sup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="rounded-[calc(var(--ui-radius)*1.5)] border border-(--ui-border-muted) overflow-hidden aspect-[16/9]">
|
||||
<UColorModeImage
|
||||
:light="`${component.path.replace('/components/', '/components/light/')}.png`"
|
||||
:dark="`${component.path.replace('/components/', '/components/dark/')}.png`"
|
||||
class="group-hover:scale-105 transition-transform size-full"
|
||||
:loading="index >= 4 ? 'lazy' : 'eager'"
|
||||
width="640"
|
||||
height="360"
|
||||
/>
|
||||
</div>
|
||||
</UPageCard>
|
||||
</UPageGrid>
|
||||
</UContainer>
|
||||
</div>
|
||||
</UMain>
|
||||
</template>
|
||||
@@ -8,7 +8,6 @@ hero:
|
||||
links:
|
||||
- label: Purchase Pro Kit
|
||||
to: 'https://nuxt.lemonsqueezy.com/buy/17213c49-621b-4c2c-9478-3a50a099003d'
|
||||
trailing-icon: i-lucide-arrow-right
|
||||
target: _blank
|
||||
- label: Free Figma Kit
|
||||
to: 'https://go.nuxt.com/figma'
|
||||
@@ -121,7 +120,6 @@ section4:
|
||||
links:
|
||||
- label: Get access now
|
||||
to: 'https://nuxt.lemonsqueezy.com/buy/17213c49-621b-4c2c-9478-3a50a099003d'
|
||||
trailing-icon: i-lucide-arrow-right
|
||||
- label: Preview UI Pro Design Kit
|
||||
to: 'https://go.nuxt.com/figma-pro'
|
||||
icon: i-logos-figma
|
||||
|
||||
@@ -93,10 +93,10 @@ onMounted(async () => {
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
<MDC :value="page.hero.title" unwrap="p" />
|
||||
<MDC :value="page.hero.title" unwrap="p" cache-key="figma-hero-title" />
|
||||
</template>
|
||||
<template #description>
|
||||
<MDC :value="page.hero.description" unwrap="p" />
|
||||
<MDC :value="page.hero.description" unwrap="p" cache-key="figma-hero-description" />
|
||||
</template>
|
||||
<!-- <img src="/pro/figma/nuxt-ui-figma.png" alt="Screnshot of the Nuxt UI Figma design kit" class="w-full h-auto border border-(--ui-border) border-b-0"> -->
|
||||
<div class="relative">
|
||||
@@ -124,7 +124,9 @@ onMounted(async () => {
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div aria-hidden="true" class="absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
<Motion as-child :initial="{ height: 0 }" :animate="{ height: 'auto' }" :transition="{ delay: 0.2, duration: 1 }">
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
</Motion>
|
||||
</UPageHero>
|
||||
<UPageSection v-bind="page.features1" :ui="{ container: 'py-16 sm:py-16 lg:py-16', features: 'mt-0' }" class="border-y border-(--ui-border)" />
|
||||
<UPageCTA
|
||||
@@ -138,10 +140,10 @@ onMounted(async () => {
|
||||
class="rounded-none bg-gradient-to-b from-(--ui-bg-muted) to-(--ui-bg)"
|
||||
>
|
||||
<template #title>
|
||||
<MDC :value="page.cta1.title" unwrap="p" />
|
||||
<MDC :value="page.cta1.title" unwrap="p" cache-key="figma-cta-1-title" />
|
||||
</template>
|
||||
<template #description>
|
||||
<MDC :value="page.cta1.description" unwrap="p" />
|
||||
<MDC :value="page.cta1.description" unwrap="p" cache-key="figma-cta-1-description" />
|
||||
</template>
|
||||
</UPageCTA>
|
||||
<UPageSection v-bind="page.section1" orientation="horizontal" :ui="{ container: 'py-16 sm:py-16 lg:py-16' }">
|
||||
@@ -187,7 +189,7 @@ onMounted(async () => {
|
||||
}"
|
||||
>
|
||||
<template #description>
|
||||
<MDC :value="page.section4.description" unwrap="p" />
|
||||
<MDC :value="page.section4.description" unwrap="p" cache-key="figma-section-4-description" />
|
||||
</template>
|
||||
<div aria-hidden="true" class="absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
<ul class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 items-start justify-center border border-(--ui-border) border-b-0 sm:divide-x divide-y lg:divide-y-0 divide-(--ui-border)">
|
||||
@@ -243,7 +245,7 @@ onMounted(async () => {
|
||||
<template #features>
|
||||
<li v-for="(feature, i) in plan.features" :key="i" class="flex items-center gap-2 min-w-0">
|
||||
<UIcon name="i-lucide-circle-check" class="size-5 shrink-0 text-(--ui-primary)" />
|
||||
<MDC :value="feature" unwrap="p" tag="span" class="text-sm truncate text-(--ui-text-accented)" />
|
||||
<MDC :value="feature" unwrap="p" tag="span" class="text-sm truncate text-(--ui-text-accented)" :cache-key="`figma-pricing-plan-${index}-feature-${i}`" />
|
||||
</li>
|
||||
</template>
|
||||
<template #button>
|
||||
@@ -279,8 +281,8 @@ onMounted(async () => {
|
||||
:items="(page.faq.items as any[])"
|
||||
class="max-w-4xl mx-auto"
|
||||
>
|
||||
<template #body="{ item }">
|
||||
<MDC :value="item.content" unwrap="p" />
|
||||
<template #body="{ item, index }">
|
||||
<MDC :value="item.content" unwrap="p" :cache-key="`figma-faq-${index}-content`" />
|
||||
</template>
|
||||
</UPageAccordion>
|
||||
</UPageSection>
|
||||
|
||||
307
docs/app/pages/index.vue
Normal file
307
docs/app/pages/index.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<script setup lang="ts">
|
||||
import { joinURL } from 'ufo'
|
||||
// @ts-expect-error yaml is not typed
|
||||
import page from '.index.yml'
|
||||
|
||||
const { url } = useSiteConfig()
|
||||
|
||||
useSeoMeta({
|
||||
titleTemplate: `%s - Nuxt UI`,
|
||||
title: page.title,
|
||||
description: page.description,
|
||||
ogTitle: `${page.title} - Nuxt UI`,
|
||||
ogDescription: page.description,
|
||||
ogImage: joinURL(url, '/og-image.png')
|
||||
})
|
||||
|
||||
const { data: components } = await useAsyncData('ui-components', () => {
|
||||
return queryCollection('content')
|
||||
.where('path', 'LIKE', '/components/%')
|
||||
.where('extension', '=', 'md')
|
||||
.where('module', 'IS NULL')
|
||||
.select('path', 'title', 'description', 'category', 'module')
|
||||
.all()
|
||||
})
|
||||
|
||||
const { data: module } = await useFetch<{
|
||||
stats: {
|
||||
downloads: number
|
||||
stars: number
|
||||
}
|
||||
contributors: {
|
||||
username: string
|
||||
}[]
|
||||
}>('https://api.nuxt.com/modules/ui', {
|
||||
key: 'stats',
|
||||
transform: ({ stats, contributors }) => ({ stats, contributors })
|
||||
})
|
||||
|
||||
const { format } = Intl.NumberFormat('en', { notation: 'compact' })
|
||||
|
||||
const contributorsRef = ref(null)
|
||||
const isContributorsInView = ref(false)
|
||||
const isContributorsHovered = useElementHover(contributorsRef)
|
||||
|
||||
useIntersectionObserver(contributorsRef, ([entry]) => {
|
||||
isContributorsInView.value = entry?.isIntersecting || false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UMain>
|
||||
<UPageHero
|
||||
orientation="horizontal"
|
||||
:ui="{
|
||||
container: 'pb-0 sm:pb-0 lg:py-0',
|
||||
title: 'lg:mt-16',
|
||||
links: 'lg:mb-16',
|
||||
description: 'text-balance'
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
The Intuitive <br> <span class="text-(--ui-primary)">Vue UI Library</span>
|
||||
</template>
|
||||
<template #description>
|
||||
{{ page.hero.description }}
|
||||
</template>
|
||||
|
||||
<template #links>
|
||||
<UButton v-for="link of page.hero.links" :key="link.label" v-bind="link" size="xl" />
|
||||
<div class="w-full my-6">
|
||||
<USeparator class="w-1/2" type="dashed" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Motion
|
||||
v-for="(feature, index) in page.hero.features"
|
||||
:key="feature.title"
|
||||
as-child
|
||||
:initial="{ opacity: 0, transform: 'translateX(-10px)' }"
|
||||
:in-view="{ opacity: 1, transform: 'translateX(0)' }"
|
||||
:transition="{ delay: 0.2 + 0.4 * index }"
|
||||
:in-view-options="{ once: true }"
|
||||
>
|
||||
<UPageFeature v-bind="feature" class="opacity-0" />
|
||||
</Motion>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<SkyBg />
|
||||
|
||||
<div class="h-[344px] lg:h-full lg:relative w-full lg:min-h-[calc(100vh-var(--ui-header-height)-1px)] overflow-hidden">
|
||||
<UPageMarquee
|
||||
pause-on-hover
|
||||
:overlay="false"
|
||||
:ui="{
|
||||
root: '[--gap:--spacing(4)] [--duration:40s] border-(--ui-border) absolute w-full left-0 border-y lg:border-x lg:border-y-0 lg:w-[calc(50%-6px)] 2xl:w-[320px] lg:flex-col',
|
||||
content: 'lg:w-auto lg:h-full lg:flex-col lg:animate-[marquee-vertical_var(--duration)_linear_infinite] lg:rtl:animate-[marquee-vertical-rtl_var(--duration)_linear_infinite] lg:h-[fit-content]'
|
||||
}"
|
||||
>
|
||||
<ULink
|
||||
v-for="component of components?.slice(0, 10)"
|
||||
:key="component.path"
|
||||
class="relative group/link aspect-video border-(--ui-border) w-[290px] xl:w-[330px] 2xl:w-[320px] 2xl:p-2 2xl:border-y"
|
||||
:to="component.path"
|
||||
>
|
||||
<UColorModeImage
|
||||
|
||||
:light="`${component.path.replace('/components/', '/components/light/')}.png`"
|
||||
:dark="`${component.path.replace('/components/', '/components/dark/')}.png`"
|
||||
class="hover:scale-105 lg:hover:scale-110 transition-transform aspect-video w-full border-x lg:border-x-0 lg:border-y border-(--ui-border) 2xl:border-y-0"
|
||||
/>
|
||||
<UBadge color="neutral" variant="outline" size="md" :label="component.title" class="hidden lg:block absolute mx-auto top-4 left-6 xl:left-4 group-hover/link:opacity-100 opacity-0 transition-all duration-300 pointer-events-none -translate-y-2 group-hover/link:translate-y-0" />
|
||||
</ULink>
|
||||
</UPageMarquee>
|
||||
|
||||
<UPageMarquee
|
||||
pause-on-hover
|
||||
reverse
|
||||
:overlay="false"
|
||||
:ui="{
|
||||
root: '[--gap:--spacing(4)] [--duration:40s] border-(--ui-border) absolute w-full mt-[180px] left-0 border-y lg:mt-auto lg:left-auto lg:border-y-0 lg:border-x lg:w-[calc(50%-6px)] 2xl:w-[320px] lg:right-0 lg:flex-col',
|
||||
content: 'lg:w-auto lg:h-full lg:flex-col lg:animate-[marquee-vertical_var(--duration)_linear_infinite] lg:rtl:animate-[marquee-vertical-rtl_var(--duration)_linear_infinite] lg:h-[fit-content] lg:[animation-direction:reverse]'
|
||||
}"
|
||||
>
|
||||
<ULink
|
||||
v-for="component of components?.slice(10, 20)"
|
||||
:key="component.path"
|
||||
class="relative group/link aspect-video border-(--ui-border) w-[290px] xl:w-[330px] 2xl:w-[320px] 2xl:p-2 2xl:border-y"
|
||||
:to="component.path"
|
||||
>
|
||||
<UColorModeImage
|
||||
:light="`${component.path.replace('/components/', '/components/light/')}.png`"
|
||||
:dark="`${component.path.replace('/components/', '/components/dark/')}.png`"
|
||||
class="hover:scale-105 lg:hover:scale-110 transition-transform aspect-video w-full border-x lg:border-x-0 lg:border-y border-(--ui-border) 2xl:border-y-0"
|
||||
/>
|
||||
<UBadge color="neutral" variant="outline" size="md" :label="component.title" class="hidden lg:block absolute mx-auto top-4 left-6 xl:left-4 group-hover/link:opacity-100 opacity-0 transition-all duration-300 pointer-events-none -translate-y-2 group-hover/link:translate-y-0" />
|
||||
</ULink>
|
||||
</UPageMarquee>
|
||||
</div>
|
||||
</UPageHero>
|
||||
|
||||
<USeparator />
|
||||
|
||||
<UPageSection :ui="{ container: 'lg:py-16' }">
|
||||
<ul class="grid grid-cols-1 gap-x-6 sm:grid-cols-2 lg:grid-cols-3 gap-y-6 lg:gap-x-8 lg:gap-y-8 xl:gap-y-10">
|
||||
<Motion
|
||||
v-for="(feature, index) in page?.features"
|
||||
:key="feature.title"
|
||||
as="li"
|
||||
:initial="{ opacity: 0, transform: 'translateY(10px)' }"
|
||||
:in-view="{ opacity: 1, transform: 'translateY(0)' }"
|
||||
:transition="{ delay: 0.1 * index }"
|
||||
:in-view-options="{ once: true }"
|
||||
class="flex items-start gap-x-3 relative group"
|
||||
>
|
||||
<NuxtLink v-if="feature.to" :to="feature.to" class="absolute inset-0 z-10" />
|
||||
|
||||
<div class="relative p-3">
|
||||
<svg class="absolute inset-0" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="6.5" x2="6.5" y2="44" stroke="var(--ui-border)" />
|
||||
<line x1="38.5" x2="38.5" y2="44" stroke="var(--ui-border)" />
|
||||
<line y1="5.5" x2="44" y2="5.5" stroke="var(--ui-border)" />
|
||||
<line y1="37.5" x2="44" y2="37.5" stroke="var(--ui-border)" />
|
||||
<circle cx="6.53613" cy="5.45508" r="1.5" fill="var(--ui-border-accented)" />
|
||||
<circle cx="38.5957" cy="5.45508" r="1.5" fill="var(--ui-border-accented)" />
|
||||
<circle cx="6.53711" cy="37.4551" r="1.5" fill="var(--ui-border-accented)" />
|
||||
<circle cx="38.5957" cy="37.4551" r="1.5" fill="var(--ui-border-accented)" />
|
||||
</svg>
|
||||
<UIcon :name="feature.icon" class="size-5 flex-shrink-0" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<h2 class="font-medium text-(--ui-text-highlighted) inline-flex items-center gap-x-1">
|
||||
{{ feature.title }}
|
||||
<UIcon v-if="feature.to" name="i-lucide-arrow-right" class="size-4 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-all duration-200 -translate-x-1 group-hover:translate-x-0" />
|
||||
</h2>
|
||||
<p class="text-sm text-(--ui-text-muted)">
|
||||
{{ feature.description }}
|
||||
</p>
|
||||
</div>
|
||||
</Motion>
|
||||
</ul>
|
||||
</UPageSection>
|
||||
|
||||
<USeparator />
|
||||
|
||||
<UPageSection
|
||||
:title="page.design_system.title"
|
||||
:description="page.design_system.description"
|
||||
:features="page.design_system.features"
|
||||
:links="page.design_system.links"
|
||||
orientation="horizontal"
|
||||
>
|
||||
<MDC :value="page.design_system.code" />
|
||||
</UPageSection>
|
||||
|
||||
<USeparator />
|
||||
|
||||
<UPageSection
|
||||
:title="page.component_customization.title"
|
||||
:features="page.component_customization.features"
|
||||
:links="page.component_customization.links"
|
||||
orientation="horizontal"
|
||||
>
|
||||
<template #description>
|
||||
<MDC :value="page.component_customization.description" />
|
||||
</template>
|
||||
|
||||
<MDC :value="page.component_customization.code" />
|
||||
</UPageSection>
|
||||
|
||||
<USeparator />
|
||||
|
||||
<UPageSection
|
||||
:title="page.community.title"
|
||||
:description="page.community.description"
|
||||
:links="page.community.links"
|
||||
orientation="horizontal"
|
||||
:ui="{ features: 'flex items-center gap-4 lg:gap-8' }"
|
||||
class="border-b border-(--ui-border)"
|
||||
>
|
||||
<template #features>
|
||||
<NuxtLink to="https://npm.chart.dev/@nuxt/ui" target="_blank" class="min-w-0">
|
||||
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
|
||||
{{ format(module?.stats?.downloads ?? 0) }}+
|
||||
</p>
|
||||
<p class="text-(--ui-text-muted) text-sm truncate">monthly downloads</p>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="https://github.com/nuxt/ui" target="_blank" class="min-w-0">
|
||||
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
|
||||
{{ format(module?.stats?.stars ?? 0) }}+
|
||||
</p>
|
||||
<p class="text-(--ui-text-muted) text-sm truncate">GitHub stars</p>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="https://github.com/nuxt/ui/graphs/contributors" target="_blank" class="min-w-0">
|
||||
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
|
||||
175+
|
||||
</p>
|
||||
<p class="text-(--ui-text-muted) text-sm truncate">Contributors</p>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<div ref="contributorsRef" class="p-4 sm:px-6 md:px-8 lg:px-12 xl:px-14 overflow-hidden flex relative">
|
||||
<LazyHomeContributors :contributors="module?.contributors" :paused="!isContributorsInView || isContributorsHovered" />
|
||||
</div>
|
||||
</UPageSection>
|
||||
|
||||
<UPageSection :ui="{ container: 'relative !pb-0 overflow-hidden' }">
|
||||
<template #title>
|
||||
Build faster with Nuxt UI <span class="text-(--ui-primary)">Pro</span>.
|
||||
</template>
|
||||
<template #description>
|
||||
A collection of premium Vue components, composables and utils built on top of Nuxt UI. <br> Focused on structure and layout, these <span class="text-(--ui-text)">responsive components</span> are designed to be the perfect <span class="text-(--ui-text)">building blocks for your next idea</span>.
|
||||
</template>
|
||||
<template #links>
|
||||
<UButton to="/pro" size="lg">
|
||||
Discover Nuxt UI Pro
|
||||
</UButton>
|
||||
<UButton to="/pro/templates" size="lg" variant="outline" trailing-icon="i-lucide-arrow-right" color="neutral">
|
||||
Explore templates
|
||||
</UButton>
|
||||
</template>
|
||||
|
||||
<StarsBg />
|
||||
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
<div class="relative h-[400px] border border-(--ui-border) bg-(--ui-bg-muted) overflow-hidden border-x-0 -mx-4 sm:-mx-6 lg:mx-0 lg:border-x w-screen lg:w-full">
|
||||
<UPageMarquee reverse orientation="vertical" :overlay="false" :ui="{ root: '[--duration:40s] absolute w-[460px] -left-[100px] -top-[300px] h-[940px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
|
||||
<img
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
:src="`/pro/blocks/image${i}.png`"
|
||||
width="460"
|
||||
height="258"
|
||||
loading="lazy"
|
||||
:alt="`Nuxt UI Pro Screenshot ${i}`"
|
||||
class="aspect-video border border-(--ui-border) rounded-[calc(var(--ui-radius)*2)] bg-white"
|
||||
>
|
||||
</UPageMarquee>
|
||||
<UPageMarquee orientation="vertical" :overlay="false" :ui="{ root: '[--duration:40s] absolute w-[460px] -top-[400px] left-[480px] h-[1160px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
|
||||
<img
|
||||
v-for="i in [5, 6, 7, 8]"
|
||||
:key="i"
|
||||
:src="`/pro/blocks/image${i}.png`"
|
||||
width="460"
|
||||
height="258"
|
||||
loading="lazy"
|
||||
:alt="`Nuxt UI Pro Screenshot ${i}`"
|
||||
class="aspect-video border border-(--ui-border) rounded-[calc(var(--ui-radius)*2)] bg-white"
|
||||
>
|
||||
</UPageMarquee>
|
||||
<UPageMarquee reverse orientation="vertical" :overlay="false" :ui="{ root: 'hidden md:flex [--duration:40s] absolute w-[460px] -top-[300px] left-[1020px] h-[1060px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
|
||||
<img
|
||||
v-for="i in [9, 10, 11, 12]"
|
||||
:key="i"
|
||||
:src="`/pro/blocks/image${i}.png`"
|
||||
width="460"
|
||||
height="258"
|
||||
:alt="`Nuxt UI Pro Screenshot ${i}`"
|
||||
loading="lazy"
|
||||
class="aspect-video border border-(--ui-border) rounded-[calc(var(--ui-radius)*2)] bg-white"
|
||||
>
|
||||
</UPageMarquee>
|
||||
</div>
|
||||
</UPageSection>
|
||||
</UMain>
|
||||
</template>
|
||||
@@ -7,17 +7,15 @@ pricing:
|
||||
freePlan:
|
||||
title: Free in development
|
||||
description: Try Nuxt UI Pro for free in development, no credit card required. Upgrade when ready to deploy.
|
||||
variant: soft
|
||||
orientation: horizontal
|
||||
button:
|
||||
label: Get started for Free
|
||||
to: '/getting-started/installation/pro/nuxt'
|
||||
color: 'neutral'
|
||||
variant: 'outline'
|
||||
variant: 'subtle'
|
||||
figma:
|
||||
title: Figma Kit Pro
|
||||
description: Get all Nuxt UI Pro components in a Figma kit to design your next application before coding. Everything you need, from wire-framing to high-fidelity web integration.
|
||||
variant: soft
|
||||
orientation: horizontal
|
||||
price: $149 - $349
|
||||
terms: Solo & Team licenses available.
|
||||
@@ -31,7 +29,7 @@ pricing:
|
||||
- Free [preview available](https://www.figma.com/design/mxXR9binOSLU3rYKZZRPXs/PREVIEW---NuxtUIPro-V3-BETA?m=auto&t=c4598Wr0rZwKPs5M-1)
|
||||
- Includes Nuxt UI [Figma Kit](https://www.figma.com/community/file/1288455405058138934)
|
||||
button:
|
||||
label: Explore Figma Kit Pricing
|
||||
label: Buy Figma Kit
|
||||
to: 'https://nuxt.lemonsqueezy.com/buy/17213c49-621b-4c2c-9478-3a50a099003d'
|
||||
color: 'neutral'
|
||||
plans:
|
||||
@@ -121,12 +119,12 @@ testimonials:
|
||||
to: 'https://www.linkedin.com/in/anthonybettini/'
|
||||
target: _blank
|
||||
avatar:
|
||||
src: 'https://media.licdn.com/dms/image/v2/C4E03AQEY3pmXsH8hDg/profile-displayphoto-shrink_200_200/profile-displayphoto-shrink_200_200/0/1519741249602?e=1743638400&v=beta&t=lw2K6vS0OOCZWGtHY1buJVkRItQCa4OQw0vzAhhpJk8'
|
||||
src: 'https://media.licdn.com/dms/image/v2/C4E03AQEY3pmXsH8hDg/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1519741249442?e=1746057600&v=beta&t=dvQfBT9ah03MPNy9cnly30ugreeCdxG4nrxV3lwKAC8'
|
||||
- quote: "Wow, Nuxt UI Pro is a total game-changer! I'm seriously impressed with the quality, attention to detail, and the insane variety of components you get. It's like hitting the jackpot for any developer. I've saved countless hours that I would've spent stressing over making my apps look good, with amazing accessible UX, and instead, I've been able to focus on the real deal – building the app itself. It's an instant buy for me, every single time. No second thoughts!"
|
||||
user:
|
||||
name: 'Yaz Jallad'
|
||||
description: 'Founder Ninjaparade Digital'
|
||||
to: 'https://twitter.com/ninjaparade/'
|
||||
to: 'https://x.com/ninjaparade/'
|
||||
target: _blank
|
||||
avatar:
|
||||
src: 'https://pbs.twimg.com/profile_images/1824690890222485504/lQ7v1AGt_400x400.jpg'
|
||||
@@ -160,8 +158,8 @@ testimonials:
|
||||
- quote: "I jumped at the chance to buy the Nuxt team's new UI kit as soon as I saw it. While I'm already a fan of Nuxt UI, the pro version takes it to a whole new level and lets me paste entire blocks into all my projects, saving me a ton of time."
|
||||
user:
|
||||
name: 'Thomas Sanlis'
|
||||
description: 'Freelance developer and designer'
|
||||
to: 'https://twitter.com/T_Zahil'
|
||||
description: 'Founder of Uneed'
|
||||
to: 'https://x.com/T_Zahil'
|
||||
target: _blank
|
||||
avatar:
|
||||
src: 'https://pbs.twimg.com/profile_images/1374040164180299791/ACw4G3nZ_400x400.jpg'
|
||||
@@ -169,7 +167,7 @@ testimonials:
|
||||
user:
|
||||
name: 'Benjamin Code'
|
||||
description: 'YouTuber and SaaS builder'
|
||||
to: 'https://twitter.com/benjamincode'
|
||||
to: 'https://x.com/benjamincode'
|
||||
target: _blank
|
||||
avatar:
|
||||
src: 'https://pbs.twimg.com/profile_images/1607353032420769793/I8qQSUfQ_400x400.jpg'
|
||||
@@ -177,15 +175,23 @@ testimonials:
|
||||
user:
|
||||
name: 'Estéban Soubiran'
|
||||
description: 'Web developer and UnJS member'
|
||||
to: 'https://twitter.com/soubiran_'
|
||||
to: 'https://x.com/soubiran_'
|
||||
target: _blank
|
||||
avatar:
|
||||
src: 'https://pbs.twimg.com/profile_images/1801649350319218689/aS_X_iTm_400x400.jpg'
|
||||
- quote: "I used to code all my components from scratch, but since I started using Nuxt UI Pro, I just can’t work without it. It has all the components I need: for landing pages, for dashboards and even for blog content! This is a no-brainer investment to ship faster, and I’ve already made my money back 10x in time saved!"
|
||||
user:
|
||||
name: 'Nico Jeannen'
|
||||
description: 'Founder of Ads Template'
|
||||
to: 'https://x.com/nico_jeannen'
|
||||
target: _blank
|
||||
avatar:
|
||||
src: 'https://pbs.twimg.com/profile_images/1754025659180187649/AAG78n19_400x400.jpg'
|
||||
- quote: "As someone who builds a lot of projects, Nuxt UI Pro has been a game-changer. It's not just about saving time – it's about having components that are thoughtfully designed and just work. The developer experience is exceptional, and I can focus on building features instead of tweaking UI components."
|
||||
user:
|
||||
name: 'Hugo Richard'
|
||||
description: 'Frontend Engineer at NuxtLabs'
|
||||
to: 'https://twitter.com/hugorcd__'
|
||||
description: 'Founder of Shelve'
|
||||
to: 'https://x.com/hugorcd__'
|
||||
target: _blank
|
||||
avatar:
|
||||
src: 'https://ipx.nuxt.com/f_auto,s_40x40/gh_avatar/hugorcd'
|
||||
|
||||
@@ -2,18 +2,18 @@ title: Build faster with Nuxt UI Pro.
|
||||
description: A collection of premium Vue components, composables and utils built on top of Nuxt UI, oriented on structure and layout and designed to be used as building blocks for your application.
|
||||
hero:
|
||||
title: Build faster with Nuxt UI [Pro]{class="text-(--ui-primary)"}.
|
||||
description: A collection of premium Vue components, composables and utils built on top of Nuxt UI. :br Focused on structure and layout, these **responsive components** are designed to be the perfect **building blocks for your next idea**.
|
||||
description: A collection of premium Vue components, composables and utils built on top of Nuxt UI. :br Focused on structure and layout, these [responsive components]{class="text-(--ui-text)"} are designed to be the perfect [building blocks for your next idea]{class="text-(--ui-text)"}.
|
||||
links:
|
||||
- label: Get started
|
||||
icon: i-lucide-arrow-right
|
||||
- label: Buy a license
|
||||
size: xl
|
||||
to: /pro/pricing
|
||||
- label: Try for free
|
||||
trailing: true
|
||||
to: /getting-started/installation/pro/nuxt
|
||||
size: xl
|
||||
- label: Purchase a license
|
||||
size: xl
|
||||
color: neutral
|
||||
variant: outline
|
||||
to: /pro/pricing
|
||||
to: /getting-started/installation/pro/nuxt
|
||||
size: xl
|
||||
trailingIcon: i-lucide-arrow-right
|
||||
features:
|
||||
title: Create stunning Vue applications faster.
|
||||
description: Nuxt UI Pro comes packed with powerful features to help you build modern, performant, accessible and responsive Nuxt applications at record speed. From pre-built UI sections to Figma design kits, every detail is crafted to speed up your development and deliver a polished user experience.
|
||||
@@ -45,18 +45,30 @@ features:
|
||||
- title: Figma Design Kits
|
||||
description: Match your development workflow with Nuxt UI & UI Pro Figma UI kits, ensuring a fast transition from design to code.
|
||||
icon: i-simple-icons-figma
|
||||
authorQuote:
|
||||
quote: Nuxt UI, born from a desire to improve Vue component development, is the go-to library for building modern web interfaces. We aim to provide you with a comprehensive set of tools to create and customize your next UI while maintaining the best developer experience.
|
||||
testimonial:
|
||||
quote: Nuxt UI has allowed me to develop my SaaS without any prior mockups. The design quality of their components and the intelligence of the DX meant that I was able to try many different layouts for my application until I found the perfect UX for my users. Nuxt UI is the ui-kit I would have dreamed of building myself, and Nuxt UI Pro makes things even easier when you want to go further with your SaaS. Kudos to the team.
|
||||
user:
|
||||
name: Benjamin Canac
|
||||
description: Author of Nuxt UI
|
||||
to: https://github.com/benjamincanac
|
||||
name: Benjamin Code
|
||||
description: YouTuber and Founder
|
||||
to: https://x.com/benjamincode
|
||||
avatar:
|
||||
src: https://github.com/benjamincanac.png
|
||||
src: https://pbs.twimg.com/profile_images/1607353032420769793/I8qQSUfQ_400x400.jpg
|
||||
# authorQuote:
|
||||
# quote: Nuxt UI, born from a desire to improve Vue component development, is the go-to library for building modern web interfaces. We aim to provide you with a comprehensive set of tools to create and customize your next UI while maintaining the best developer experience.
|
||||
# user:
|
||||
# name: Benjamin Canac
|
||||
# description: Author of Nuxt UI
|
||||
# to: https://github.com/benjamincanac
|
||||
# avatar:
|
||||
# src: https://github.com/benjamincanac.png
|
||||
mainSection:
|
||||
title: Meet the [Pro Components]{class="text-(--ui-primary)"}.
|
||||
description: Code with 50+ components and sections of Nuxt UI Pro to build your next application by reducing the amount of code you need to write.
|
||||
sections:
|
||||
- title: The freedom to build anything
|
||||
description: Nuxt UI Pro ships with an extensive set of advanced components that cover a wide range of use-cases. Carefully crafted to reduce boilerplate code without sacrificing flexibility.
|
||||
id: features
|
||||
reverse: true
|
||||
features:
|
||||
- name: Fully customizable
|
||||
description: Like Nuxt UI, change the style of any component from your App Config or customize them specifically through the ui prop.
|
||||
@@ -99,9 +111,8 @@ sections:
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
- title: The flexibility to control your data
|
||||
- title: The flexibility to display your data
|
||||
description: Although you can use any data source you want, Nuxt UI Pro is fully integrated with Nuxt Content and provides additional features when the module is detected.
|
||||
reverse: true
|
||||
features:
|
||||
- name: 'Write Markdown with ease'
|
||||
description: Nuxt UI Pro overrides Nuxt Content prose components to make them awesome but also adds new ones like Callout, CodeGroup, Field, etc.
|
||||
@@ -110,7 +121,7 @@ sections:
|
||||
description: 'Nuxt UI Pro ships with a ready to use command palette component. No need to setup Algolia DocSearch anymore.'
|
||||
icon: i-lucide-search
|
||||
links:
|
||||
- label: Nuxt Content integration
|
||||
- label: Nuxt Content Integration
|
||||
to: /getting-started/content
|
||||
icon: i-lucide-arrow-right
|
||||
trailing: true
|
||||
@@ -140,35 +151,14 @@ templates:
|
||||
headline: Templates
|
||||
title: Kickstart with Nuxt UI Pro in seconds
|
||||
description: Choose from a variety of templates to get started with Nuxt UI Pro in seconds. Each template is designed to help you build beautiful and responsive Nuxt applications in minutes.
|
||||
items:
|
||||
- title: Landing
|
||||
description: A template for building a landing page with Nuxt UI Pro.
|
||||
icon: i-lucide-layout
|
||||
to: 'https://landing-template.nuxt.dev/'
|
||||
image: '/pro/templates/landing.png'
|
||||
- title: Saas
|
||||
description: A template for building a SaaS application with Nuxt UI Pro.
|
||||
icon: i-lucide-cloud
|
||||
to: 'https://saas-template.nuxt.dev/'
|
||||
image: '/pro/templates/saas.png'
|
||||
- title: Docs
|
||||
description: A template for building a documentation site with Nuxt UI Pro.
|
||||
icon: i-lucide-book
|
||||
to: 'https://docs-template.nuxt.dev/'
|
||||
image: '/pro/templates/docs.png'
|
||||
- title: Dashboard
|
||||
description: A template for building a dashboard with Nuxt UI Pro.
|
||||
icon: i-lucide-chart-bar
|
||||
to: 'https://dashboard-template.nuxt.dev/'
|
||||
image: '/pro/templates/dashboard.png'
|
||||
cta:
|
||||
title: Start with Nuxt UI Pro today!
|
||||
description: Nuxt UI Pro is free in development, but you need a license to use it in production.
|
||||
links:
|
||||
- label: Purchase a license
|
||||
- label: Buy a license
|
||||
to: '/pro/pricing'
|
||||
icon: i-lucide-shopping-cart
|
||||
- label: License
|
||||
to: '/getting-started/license'
|
||||
trailingIcon: i-lucide-circle-help
|
||||
variant: subtle
|
||||
- label: Try for free
|
||||
to: /getting-started/installation/pro/nuxt
|
||||
variant: outline
|
||||
color: neutral
|
||||
trailingIcon: i-lucide-arrow-right
|
||||
|
||||
@@ -1,22 +1,88 @@
|
||||
title: Official Nuxt UI [Pro]{.text-(--ui-primary)} Templates
|
||||
description: 'Ready to use templates powered by our premium Vue components and Nuxt Content.<br class="hidden lg:block"> The templates are responsive, accessible and easy to customize so you can get started in no time.'
|
||||
head.title: Templates
|
||||
head.description: 'Ready to use templates powered by our premium Vue components and Nuxt Content. The templates are responsive, accessible and easy to customize so you can get started in no time.'
|
||||
title: Official Nuxt UI Pro Templates
|
||||
description: 'Ready to use templates powered by our premium Vue components and Nuxt Content. The templates are responsive, accessible and easy to customize so you can get started in no time.'
|
||||
hero:
|
||||
title: Ship [in minutes]{.text-(--ui-primary)} with :br Nuxt UI Pro Templates
|
||||
description: 'Ready to use templates powered by our premium Vue components and Nuxt Content.<br class="hidden lg:block"> The templates are responsive, accessible and easy to customize so you can get started in no time.'
|
||||
navigation: false
|
||||
links:
|
||||
- label: Get started
|
||||
to: /getting-started/installation/pro/nuxt#use-an-official-template
|
||||
color: neutral
|
||||
- label: Buy a license
|
||||
color: primary
|
||||
size: xl
|
||||
trailingIcon: i-lucide-arrow-right
|
||||
- label: Purchase a license
|
||||
to: /pro/pricing
|
||||
- label: Try for free
|
||||
to: /getting-started/installation/pro/nuxt#use-an-official-template
|
||||
size: xl
|
||||
color: neutral
|
||||
variant: outline
|
||||
to: /pro/pricing
|
||||
trailingIcon: i-lucide-arrow-right
|
||||
templates:
|
||||
- title: 'Dashboard'
|
||||
description: "A template to illustrate how to build your own dashboard with the 15+ latest Nuxt UI Pro components, designed specifically to create a consistent look and feel."
|
||||
icon: i-lucide-bar-chart-big
|
||||
thumbnail:
|
||||
dark: https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2Rhc2hib2FyZC10ZW1wbGF0ZS5udXh0LmRldiIsImlhdCI6MTczOTQ2MzU2N30._VElt4uvLjvAMdnTLytCInOajMElzWDKbmvOaMZhZUI.jpg?theme=dark
|
||||
light: https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2Rhc2hib2FyZC10ZW1wbGF0ZS5udXh0LmRldiIsImlhdCI6MTczOTQ2MzU2N30._VElt4uvLjvAMdnTLytCInOajMElzWDKbmvOaMZhZUI.jpg?theme=light
|
||||
features:
|
||||
- title: Mix with SaaS template for a complete solution
|
||||
icon: i-lucide-puzzle
|
||||
- title: Includes custom components for charts, date pickers, etc.
|
||||
icon: i-lucide-bar-chart-big
|
||||
- title: Resizable multi-column layout
|
||||
icon: i-lucide-columns-3
|
||||
links:
|
||||
- label: Preview
|
||||
to: https://dashboard-template.nuxt.dev
|
||||
target: _blank
|
||||
leadingIcon: i-logos-nuxt-icon
|
||||
trailingIcon: i-lucide-arrow-up-right
|
||||
color: neutral
|
||||
- label: Nuxt Template
|
||||
to: https://github.com/nuxt-ui-pro/dashboard
|
||||
target: _blank
|
||||
icon: i-simple-icons-github
|
||||
color: neutral
|
||||
variant: outline
|
||||
- label: Preview
|
||||
to: https://vue-dashboard-template.nuxt.dev
|
||||
target: _blank
|
||||
leadingIcon: i-logos-vue
|
||||
trailingIcon: i-lucide-arrow-up-right
|
||||
color: neutral
|
||||
- label: Vue Template
|
||||
to: https://github.com/nuxt-ui-pro/dashboard-vue
|
||||
target: _blank
|
||||
icon: i-simple-icons-github
|
||||
color: neutral
|
||||
variant: outline
|
||||
- title: 'SaaS'
|
||||
description: "A fully built SaaS application to launch your next project. It includes a landing page, a pricing page, a documentation and a blog which can customized easily from the `content/` directory."
|
||||
icon: i-lucide-cloud
|
||||
thumbnail:
|
||||
dark: https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL3NhYXMtdGVtcGxhdGUubnV4dC5kZXYiLCJpYXQiOjE3Mzk0NjM0NDh9.tgzUQaw6XswUPPVbOXazuWwoTHJODg155CYt1xfzIdM.jpg?theme=dark
|
||||
light: https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL3NhYXMtdGVtcGxhdGUubnV4dC5kZXYiLCJpYXQiOjE3Mzk0NjM0NDh9.tgzUQaw6XswUPPVbOXazuWwoTHJODg155CYt1xfzIdM.jpg?theme=light
|
||||
features:
|
||||
- title: Includes Landing & Docs sections
|
||||
icon: i-lucide-grid-2x2-plus
|
||||
- title: Customizable command palette
|
||||
icon: i-lucide-command
|
||||
- title: Authentication pages (login, register)
|
||||
icon: i-lucide-user-round-check
|
||||
links:
|
||||
- label: Preview
|
||||
to: https://saas-template.nuxt.dev
|
||||
target: _blank
|
||||
leadingIcon: i-logos-nuxt-icon
|
||||
trailingIcon: i-lucide-arrow-up-right
|
||||
color: neutral
|
||||
- label: Nuxt Template
|
||||
to: https://github.com/nuxt-ui-pro/saas
|
||||
target: _blank
|
||||
variant: outline
|
||||
icon: i-simple-icons-github
|
||||
color: neutral
|
||||
- title: 'Landing'
|
||||
description: "A landing page template you can use as a starting point for your next idea. You can change the content easily in [`content/index.yml`](https://github.com/nuxt-ui-pro/landing/blob/v3/content/index.yml)."
|
||||
icon: i-lucide-layout
|
||||
thumbnail:
|
||||
dark: https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2xhbmRpbmctdGVtcGxhdGUubnV4dC5kZXYiLCJpYXQiOjE3Mzk0NjMzNzV9.ja2nUDVOoIFvyaMmg9Jn51uNMoYYt4WA1KWUQBWwUPo.jpg?theme=dark
|
||||
light: https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2xhbmRpbmctdGVtcGxhdGUubnV4dC5kZXYiLCJpYXQiOjE3Mzk0NjMzNzV9.ja2nUDVOoIFvyaMmg9Jn51uNMoYYt4WA1KWUQBWwUPo.jpg?theme=light
|
||||
@@ -28,19 +94,21 @@ templates:
|
||||
- title: Write content in YAML
|
||||
icon: i-simple-icons-yaml
|
||||
links:
|
||||
- label: Live preview
|
||||
- label: Preview
|
||||
to: https://landing-template.nuxt.dev
|
||||
target: _blank
|
||||
leadingIcon: i-logos-nuxt-icon
|
||||
trailingIcon: i-lucide-arrow-up-right
|
||||
color: neutral
|
||||
- label: Use this template
|
||||
to: https://github.com/nuxt-ui-pro/landing/tree/v3
|
||||
- label: Nuxt Template
|
||||
to: https://github.com/nuxt-ui-pro/landing
|
||||
target: _blank
|
||||
icon: i-simple-icons-github
|
||||
color: neutral
|
||||
variant: outline
|
||||
- title: 'Docs'
|
||||
description: "A ready-to-use documentation template integrated with [Nuxt Content](https://content.nuxt.com). You can start writing your docs right away inside the [`content/`](https://github.com/nuxt-ui-pro/docs/tree/v3/content) directory."
|
||||
icon: i-lucide-book
|
||||
thumbnail:
|
||||
dark: https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RvY3MtdGVtcGxhdGUubnV4dC5kZXYiLCJpYXQiOjE3Mzk0NjM0MTd9.ltVAqPgKG38O01X1zl6MXfrJc55nf9OewXNFjpZ_2JY.jpg?theme=dark
|
||||
light: https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RvY3MtdGVtcGxhdGUubnV4dC5kZXYiLCJpYXQiOjE3Mzk0NjM0MTd9.ltVAqPgKG38O01X1zl6MXfrJc55nf9OewXNFjpZ_2JY.jpg?theme=light
|
||||
@@ -52,60 +120,51 @@ templates:
|
||||
- title: Full-text search out of the box
|
||||
icon: i-lucide-search
|
||||
links:
|
||||
- label: Live preview
|
||||
- label: Preview
|
||||
to: https://docs-template.nuxt.dev
|
||||
target: _blank
|
||||
leadingIcon: i-logos-nuxt-icon
|
||||
trailingIcon: i-lucide-arrow-up-right
|
||||
color: neutral
|
||||
- label: Use this template
|
||||
- label: Nuxt Template
|
||||
to: https://github.com/nuxt-ui-pro/docs/tree/v3
|
||||
target: _blank
|
||||
variant: outline
|
||||
icon: i-simple-icons-github
|
||||
color: neutral
|
||||
- title: 'SaaS'
|
||||
description: "A fully built SaaS application to launch your next project. It includes a landing page, a pricing page, a documentation and a blog which can customized easily from the `content/` directory."
|
||||
- title: 'Starter'
|
||||
description: "A minimal starter template to build your own project with Nuxt UI Pro components."
|
||||
icon: i-lucide-flower
|
||||
thumbnail:
|
||||
dark: https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL3VpLXByby1zdGFydGVyLm51eHQuZGV2IiwiaWF0IjoxNzM5NDYzMzk4fQ.XLzPkSW6nRbPW07QO1RkMwz_RAPA4KfeyrWrK3li9YI.jpg?theme=dark
|
||||
light: https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL3VpLXByby1zdGFydGVyLm51eHQuZGV2IiwiaWF0IjoxNzM5NDYzMzk4fQ.XLzPkSW6nRbPW07QO1RkMwz_RAPA4KfeyrWrK3li9YI.jpg?theme=light
|
||||
features:
|
||||
- title: Includes Landing & Docs sections
|
||||
icon: i-lucide-grid-2x2-plus
|
||||
- title: Customizable command palette
|
||||
icon: i-lucide-command
|
||||
- title: Authentication pages (login, register)
|
||||
icon: i-lucide-user-round-check
|
||||
- title: ESLint Configured
|
||||
icon: i-simple-icons-eslint
|
||||
- title: Nuxt 4 Compatibility Enabled
|
||||
icon: i-simple-icons-nuxtdotjs
|
||||
links:
|
||||
- label: Live preview (soon)
|
||||
to: https://saas-template.nuxt.dev
|
||||
- label: Preview
|
||||
to: https://ui-pro-starter.nuxt.dev
|
||||
target: _blank
|
||||
leadingIcon: i-logos-nuxt-icon
|
||||
trailingIcon: i-lucide-arrow-up-right
|
||||
color: neutral
|
||||
disabled: true
|
||||
- label: Use this template (soon)
|
||||
to: https://github.com/nuxt-ui-pro/saas/tree/v3
|
||||
- label: Nuxt Template
|
||||
to: https://github.com/nuxt-ui-pro/starter
|
||||
target: _blank
|
||||
variant: outline
|
||||
icon: i-simple-icons-github
|
||||
color: neutral
|
||||
disabled: true
|
||||
- title: 'Dashboard'
|
||||
description: "A template to illustrate how to build your own dashboard with the 15+ latest Nuxt UI Pro components, designed specifically to create a consistent look and feel."
|
||||
features:
|
||||
- title: Mix with SaaS template for a complete solution
|
||||
icon: i-lucide-puzzle
|
||||
- title: Includes custom components for charts, date pickers, etc.
|
||||
icon: i-lucide-bar-chart-big
|
||||
- title: Resizable multi-column layout
|
||||
icon: i-lucide-columns-3
|
||||
links:
|
||||
- label: Live preview (soon)
|
||||
to: https://dashboard-template.nuxt.dev
|
||||
- label: Preview
|
||||
to: https://ui-pro-starter-vue.nuxt.dev
|
||||
target: _blank
|
||||
leadingIcon: i-logos-vue
|
||||
trailingIcon: i-lucide-arrow-up-right
|
||||
color: neutral
|
||||
disabled: true
|
||||
- label: Use this template (soon)
|
||||
to: https://github.com/nuxt-ui-pro/dashboard/tree/v3
|
||||
- label: Vue Template
|
||||
to: https://github.com/nuxt-ui-pro/starter-vue
|
||||
target: _blank
|
||||
variant: outline
|
||||
icon: i-simple-icons-github
|
||||
color: neutral
|
||||
variant: outline
|
||||
disabled: true
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
import { joinURL } from 'ufo'
|
||||
|
||||
const title = 'Activate your Nuxt UI Pro License'
|
||||
const description = 'Enable Nuxt UI Pro in your production projects by activating your license key received by email and get invited to the GitHub private repository.'
|
||||
|
||||
const route = useRoute()
|
||||
const { url } = useSiteConfig()
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: `${title} - Nuxt UI Pro`,
|
||||
ogDescription: description
|
||||
})
|
||||
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: 'Pro'
|
||||
ogDescription: description,
|
||||
ogImage: joinURL(url, '/pro/og-image.png')
|
||||
})
|
||||
|
||||
const schema = z.object({
|
||||
@@ -70,46 +69,50 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPageHero headline="License Activation" :title="title" :description="description">
|
||||
<template #top>
|
||||
<div class="absolute z-[-1] rounded-full bg-(--ui-primary) blur-[300px] size-60 sm:size-80 transform -translate-x-1/2 left-1/2 -translate-y-80" />
|
||||
<UMain>
|
||||
<UPageHero headline="License Activation" :title="title" :description="description" :ui="{ container: 'relative overflow-hidden', wrapper: 'lg:px-12', description: 'text-pretty' }">
|
||||
<StarsBg />
|
||||
</template>
|
||||
<div>
|
||||
<UCard class="lg:w-1/2 m-auto backdrop-blur-sm">
|
||||
<UForm
|
||||
:schema="schema"
|
||||
:validate-on="['blur']"
|
||||
:state="state"
|
||||
class="space-y-4"
|
||||
@submit="submit"
|
||||
>
|
||||
<UFormField label="License Key" name="license">
|
||||
<UInput v-model="state.license" autocomplete="off" :ui="{ root: 'flex' }" />
|
||||
</UFormField>
|
||||
<UFormField label="GitHub Username" name="username">
|
||||
<UInput v-model="state.username" autocomplete="off" :ui="{ root: 'flex' }" />
|
||||
</UFormField>
|
||||
<UButton type="submit" :loading="activating" :disabled="state.license?.length !== 36 || !state.username">
|
||||
Activate the license
|
||||
</UButton>
|
||||
<UAlert v-if="successMessage" color="success" variant="subtle" :title="successMessage">
|
||||
<template #description>
|
||||
The GitHub invitation to <NuxtLink to="https://github.com/nuxt/ui-pro/invitations" external target="_blank" class="font-bold text-primary hover:underline">
|
||||
Nuxt UI Pro repository
|
||||
</NuxtLink> has been sent and
|
||||
you can now use your license key in your projects, checkout the
|
||||
<NuxtLink class="font-bold underline" to="/pro/getting-started/installation">
|
||||
installation guide
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</UAlert>
|
||||
<UAlert v-else-if="errorMessage" color="error" variant="subtle" :title="errorMessage" />
|
||||
</UForm>
|
||||
</UCard>
|
||||
<p class="text-sm text-center text-neutral-500 dark:text-neutral-400 my-4">
|
||||
If you purchased a license with multiple seats, activate the license key for each of your team members.
|
||||
</p>
|
||||
</div>
|
||||
</UPageHero>
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
|
||||
<div class="px-4 py-10 lg:border border-(--ui-border) bg-(--ui-bg)">
|
||||
<div class="max-w-xl mx-auto">
|
||||
<UForm
|
||||
:schema="schema"
|
||||
:validate-on="['blur']"
|
||||
:state="state"
|
||||
class="space-y-4"
|
||||
@submit="submit"
|
||||
>
|
||||
<UFormField label="License Key" name="license">
|
||||
<UInput v-model="state.license" autocomplete="off" :ui="{ root: 'flex' }" />
|
||||
</UFormField>
|
||||
<UFormField label="GitHub Username" name="username">
|
||||
<UInput v-model="state.username" autocomplete="off" :ui="{ root: 'flex' }" />
|
||||
</UFormField>
|
||||
<UButton type="submit" :loading="activating" :disabled="state.license?.length !== 36 || !state.username">
|
||||
Activate the license
|
||||
</UButton>
|
||||
<UAlert v-if="successMessage" color="success" variant="subtle" :title="successMessage">
|
||||
<template #description>
|
||||
The GitHub invitation to <NuxtLink to="https://github.com/nuxt/ui-pro/invitations" external target="_blank" class="font-bold text-primary hover:underline">
|
||||
Nuxt UI Pro repository
|
||||
</NuxtLink> has been sent and
|
||||
you can now use your license key in your projects, checkout the
|
||||
<NuxtLink class="font-bold underline" to="/pro/getting-started/installation">
|
||||
installation guide
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</UAlert>
|
||||
<UAlert v-else-if="errorMessage" color="error" variant="subtle" :title="errorMessage" />
|
||||
</UForm>
|
||||
|
||||
<ProseHr />
|
||||
|
||||
<ProseNote>
|
||||
If you purchased a license with multiple seats, activate the license key for each member of your team.
|
||||
</ProseNote>
|
||||
</div>
|
||||
</div>
|
||||
</UPageHero>
|
||||
</UMain>
|
||||
</template>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { joinURL } from 'ufo'
|
||||
// @ts-expect-error yaml is not typed
|
||||
import page from '.content/pro.yml'
|
||||
// @ts-expect-error yaml is not typed
|
||||
import templatesPage from '.content/templates.yml'
|
||||
|
||||
const { url } = useSiteConfig()
|
||||
|
||||
useSeoMeta({
|
||||
title: page.title,
|
||||
ogTitle: page.title,
|
||||
ogImage: joinURL(url, '/pro/og-image.png'),
|
||||
description: page.description,
|
||||
ogDescription: page.description
|
||||
})
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: 'Pro'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -18,66 +21,153 @@ defineOgImageComponent('Docs', {
|
||||
<UPageHero
|
||||
:links="page.hero.links"
|
||||
class="relative"
|
||||
orientation="horizontal"
|
||||
:ui="{
|
||||
container: 'relative !pb-0'
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
<MDC :value="page.hero.title" unwrap="p" />
|
||||
<MDC :value="page.hero.title" tag="span" unwrap="p" cache-key="pro-hero-title" />
|
||||
</template>
|
||||
<template #description>
|
||||
<MDC :value="page.hero.description" unwrap="p" />
|
||||
</template>
|
||||
<template #top>
|
||||
<div class="absolute z-[-1] rounded-full bg-(--ui-primary) blur-[300px] size-60 sm:size-80 transform -translate-x-1/2 left-1/2 -translate-y-80" />
|
||||
<StarsBg />
|
||||
<MDC :value="page.hero.description" tag="span" unwrap="p" cache-key="pro-hero-description" />
|
||||
</template>
|
||||
|
||||
<PromotionalVideo />
|
||||
<StarsBg />
|
||||
|
||||
<Motion as-child :initial="{ height: 0 }" :animate="{ height: 'auto' }" :transition="{ delay: 0.2, duration: 1 }">
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
</Motion>
|
||||
<div class="relative h-[400px] border border-(--ui-border) bg-(--ui-bg-muted) overflow-hidden border-x-0 -mx-4 sm:-mx-6 lg:mx-0 lg:border-x w-screen lg:w-full">
|
||||
<UPageMarquee reverse orientation="vertical" :overlay="false" :ui="{ root: '[--duration:40s] absolute w-[460px] -left-[100px] -top-[300px] h-[940px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
|
||||
<img
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
:src="`/pro/blocks/image${i}.png`"
|
||||
width="460"
|
||||
height="258"
|
||||
:alt="`Nuxt UI Pro Screenshot ${i}`"
|
||||
class="aspect-video border border-(--ui-border) rounded-[calc(var(--ui-radius)*2)] bg-white"
|
||||
>
|
||||
</UPageMarquee>
|
||||
<UPageMarquee orientation="vertical" :overlay="false" :ui="{ root: '[--duration:40s] absolute w-[460px] -top-[400px] left-[480px] h-[1160px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
|
||||
<img
|
||||
v-for="i in [5, 6, 7, 8]"
|
||||
:key="i"
|
||||
:src="`/pro/blocks/image${i}.png`"
|
||||
width="460"
|
||||
height="258"
|
||||
:alt="`Nuxt UI Pro Screenshot ${i}`"
|
||||
class="aspect-video border border-(--ui-border) rounded-[calc(var(--ui-radius)*2)] bg-white"
|
||||
>
|
||||
</UPageMarquee>
|
||||
<UPageMarquee reverse orientation="vertical" :overlay="false" :ui="{ root: 'hidden md:flex [--duration:40s] absolute w-[460px] -top-[300px] left-[1020px] h-[1060px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
|
||||
<img
|
||||
v-for="i in [9, 10, 11, 12]"
|
||||
:key="i"
|
||||
:src="`/pro/blocks/image${i}.png`"
|
||||
width="460"
|
||||
height="258"
|
||||
:alt="`Nuxt UI Pro Screenshot ${i}`"
|
||||
class="aspect-video border border-(--ui-border) rounded-[calc(var(--ui-radius)*2)] bg-white"
|
||||
>
|
||||
</UPageMarquee>
|
||||
</div>
|
||||
</UPageHero>
|
||||
|
||||
<UPageCTA
|
||||
variant="outline"
|
||||
class="rounded-none"
|
||||
:ui="{
|
||||
container: 'sm:py-12 lg:py-12 sm:gap-4',
|
||||
description: 'sm:text-base'
|
||||
}"
|
||||
>
|
||||
<template #description>
|
||||
<Motion :initial="{ opacity: 0, transform: 'translateY(10px)' }" :in-view="{ opacity: 1, transform: 'translateY(0)' }" :in-view-options="{ once: true }" :transition="{ delay: 0.2 }">
|
||||
<MDC :value="page.testimonial.quote" tag="span" unwrap="p" class="before:content-[open-quote] after:content-[close-quote]" cache-key="pro-testimonial-quote" />
|
||||
</Motion>
|
||||
</template>
|
||||
<Motion :initial="{ opacity: 0, transform: 'translateY(10px)' }" :in-view="{ opacity: 1, transform: 'translateY(0)' }" :in-view-options="{ once: true }" :transition="{ delay: 0.3 }">
|
||||
<UUser
|
||||
v-bind="page.testimonial.user"
|
||||
class="justify-center"
|
||||
/>
|
||||
</Motion>
|
||||
</UPageCTA>
|
||||
<UPageSection
|
||||
v-bind="page.features"
|
||||
:ui="{ title: 'text-left', description: 'text-left' }"
|
||||
/>
|
||||
:ui="{
|
||||
title: 'text-left',
|
||||
description: 'text-left',
|
||||
container: 'relative',
|
||||
wrapper: 'sm:px-8'
|
||||
}"
|
||||
class="border-t border-(--ui-border)"
|
||||
>
|
||||
<Motion as-child :initial="{ height: 0 }" :in-view="{ height: 'auto' }" :transition="{ delay: 0.4, duration: 1 }">
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
</Motion>
|
||||
</UPageSection>
|
||||
|
||||
<UPageCTA
|
||||
:description="page.authorQuote.quote"
|
||||
variant="soft"
|
||||
class="rounded-none"
|
||||
:ui="{ container: 'sm:py-12 lg:py-12 sm:gap-8', description: 'before:content-[open-quote] after:content-[close-quote]' }"
|
||||
v-if="page.mainSection"
|
||||
variant="naked"
|
||||
:ui="{
|
||||
container: 'lg:grid-cols-0 !gap-0 px-4 sm:px-6 lg:px-8',
|
||||
wrapper: 'grid grid-cols-1 lg:grid-cols-2',
|
||||
description: 'lg:mt-0' }"
|
||||
orientation="horizontal"
|
||||
class="rounded-none border-t border-(--ui-border) bg-gradient-to-b from-(--ui-bg-elevated)/50 to-(--ui-bg)"
|
||||
>
|
||||
<UUser
|
||||
v-bind="page.authorQuote.user"
|
||||
size="xl"
|
||||
class="justify-center"
|
||||
/>
|
||||
<template #title>
|
||||
<MDC :value="page.mainSection.title" tag="span" unwrap="p" cache-key="pro-main-section-title" />
|
||||
</template>
|
||||
<template #description>
|
||||
<MDC :value="page.mainSection.description" tag="span" unwrap="p" cache-key="pro-main-section-description" />
|
||||
</template>
|
||||
</UPageCTA>
|
||||
|
||||
<UPageSection
|
||||
v-for="(section, index) in page.sections"
|
||||
:key="index"
|
||||
v-bind="section"
|
||||
:title="section.title"
|
||||
:description="section.description"
|
||||
:links="section.links"
|
||||
:reverse="section.reverse"
|
||||
:features="section.features"
|
||||
orientation="horizontal"
|
||||
:class="{ 'border-b border-(--ui-border)': index === page.sections.length - 1 }"
|
||||
:ui="{
|
||||
container: index === 0 ? 'pb-0 sm:pb-0 lg:pb-0 py-16 sm:py-16 lg:py-16' : ''
|
||||
}"
|
||||
>
|
||||
<MDC :value="section.code" />
|
||||
<MDC :value="section.code" :cache-key="`pro-section-${index}-code`" />
|
||||
</UPageSection>
|
||||
|
||||
<UPageSection
|
||||
id="templates"
|
||||
v-bind="page.templates"
|
||||
class="overflow-hidden"
|
||||
class="overflow-hidden border-x border-(--ui-border)"
|
||||
:ui="{ container: 'relative' }"
|
||||
>
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
<UCarousel
|
||||
v-slot="{ item }"
|
||||
loop
|
||||
arrows
|
||||
dots
|
||||
wheel-gestures
|
||||
:autoplay="{ delay: 3000 }"
|
||||
:items="(page.templates.items as any[])"
|
||||
:ui="{ item: 'basis-1/2', container: 'py-2' }"
|
||||
:items="(templatesPage.templates as any[])"
|
||||
:ui="{
|
||||
item: 'basis-1/2',
|
||||
container: 'py-2',
|
||||
viewport: 'border-x border-(--ui-border)',
|
||||
arrows: 'hidden 2xl:block'
|
||||
}"
|
||||
>
|
||||
<UPageCard
|
||||
:to="item.to"
|
||||
:description="item.description"
|
||||
:to="item.links[0].to"
|
||||
target="_blank"
|
||||
variant="subtle"
|
||||
class="group"
|
||||
:ui="{ container: 'p-4 sm:p-4', title: 'flex items-center gap-1' }"
|
||||
>
|
||||
@@ -87,11 +177,13 @@ defineOgImageComponent('Docs', {
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</template>
|
||||
<img
|
||||
:src="item.image"
|
||||
<UColorModeImage
|
||||
:light="item.thumbnail.light"
|
||||
:dark="item.thumbnail.dark"
|
||||
:alt="item.title"
|
||||
class="rounded-lg grayscale group-hover:grayscale-0 transition-all duration-200 ease-in-out"
|
||||
>
|
||||
class="rounded-lg w-full border border-(--ui-border) aspect-video"
|
||||
loading="lazy"
|
||||
/>
|
||||
</UPageCard>
|
||||
</UCarousel>
|
||||
</UPageSection>
|
||||
@@ -100,12 +192,31 @@ defineOgImageComponent('Docs', {
|
||||
|
||||
<UPageCTA
|
||||
v-bind="page.cta"
|
||||
variant="naked"
|
||||
variant="subtle"
|
||||
class="overflow-hidden"
|
||||
orientation="horizontal"
|
||||
>
|
||||
<div class="absolute rounded-full dark:bg-(--ui-primary) blur-[250px] size-40 sm:size-50 transform -translate-x-1/2 left-1/2 -translate-y-80" />
|
||||
|
||||
<StarsBg />
|
||||
|
||||
<video
|
||||
class="rounded-[var(--ui-radius)] z-10"
|
||||
preload="none"
|
||||
poster="https://res.cloudinary.com/nuxt/video/upload/so_3.3/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.jpg"
|
||||
:controls="true"
|
||||
>
|
||||
<source
|
||||
src="https://res.cloudinary.com/nuxt/video/upload/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.webm"
|
||||
type="video/webm"
|
||||
>
|
||||
<source
|
||||
src="https://res.cloudinary.com/nuxt/video/upload/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.mp4"
|
||||
type="video/mp4"
|
||||
>
|
||||
<source
|
||||
src="https://res.cloudinary.com/nuxt/video/upload/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.ogg"
|
||||
type="video/ogg"
|
||||
>
|
||||
</video>
|
||||
</UPageCTA>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { joinURL } from 'ufo'
|
||||
// @ts-expect-error yaml is not typed
|
||||
import page from '.content/pricing.yml'
|
||||
|
||||
const { url } = useSiteConfig()
|
||||
|
||||
useSeoMeta({
|
||||
title: page.title,
|
||||
ogTitle: page.title,
|
||||
description: page.description,
|
||||
ogDescription: page.description
|
||||
})
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: 'Pro'
|
||||
ogDescription: page.description,
|
||||
ogImage: joinURL(url, '/pro/og-image.png')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -18,23 +19,23 @@ defineOgImageComponent('Docs', {
|
||||
<UPageHero
|
||||
class="relative"
|
||||
:description="page.pricing.description"
|
||||
:ui="{
|
||||
container: 'relative lg:!pb-0'
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
<MDC :value="page.pricing.title" unwrap="p" />
|
||||
<MDC :value="page.pricing.title" unwrap="p" cache-key="pro-pricing-title" />
|
||||
</template>
|
||||
<template #top>
|
||||
<div class="absolute z-[-1] rounded-full bg-(--ui-primary) blur-[300px] size-60 sm:size-80 transform -translate-x-1/2 left-1/2 -translate-y-80" />
|
||||
<StarsBg />
|
||||
</template>
|
||||
<UContainer>
|
||||
|
||||
<StarsBg />
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
<div class="flex flex-col bg-(--ui-bg) gap-8 lg:gap-0">
|
||||
<UPricingPlan
|
||||
v-bind="page.pricing.freePlan"
|
||||
class="mb-16"
|
||||
variant="naked"
|
||||
class="lg:rounded-none border-x border-(--ui-border) border-t border-b lg:border-b-0"
|
||||
/>
|
||||
<UPricingPlans
|
||||
class="mb-16"
|
||||
scale
|
||||
>
|
||||
<UPricingPlans compact>
|
||||
<UPricingPlan
|
||||
v-for="(plan, index) in page.pricing.plans"
|
||||
:key="index"
|
||||
@@ -43,29 +44,31 @@ defineOgImageComponent('Docs', {
|
||||
:price="plan.price"
|
||||
:billing-period="plan.billing_period"
|
||||
:billing-cycle="plan.billing_cycle"
|
||||
:highlight="plan.highlight"
|
||||
:scale="plan.highlight"
|
||||
variant="soft"
|
||||
:variant="plan.highlight ? 'soft' : 'outline'"
|
||||
:class="['lg:rounded-none', { 'border-2 lg:border lg:border-x-0 border-(--ui-primary) lg:border-(--ui-border)': plan.highlight }]"
|
||||
:features="plan.features"
|
||||
:button="plan.button"
|
||||
/>
|
||||
</UPricingPlans>
|
||||
<UPricingPlan
|
||||
v-bind="page.pricing.figma"
|
||||
variant="naked"
|
||||
class="lg:rounded-none border lg:border-y-0 border-(--ui-border)"
|
||||
>
|
||||
<template #features>
|
||||
<li v-for="(feature, index) in page.pricing.figma.features" :key="index" class="flex items-center gap-2 min-w-0">
|
||||
<UIcon name="i-lucide-circle-check" class="size-5 text-(--ui-primary) shrink-0" />
|
||||
<MDC :value="feature" unwrap="p" class="text-sm truncate text-(--ui-text-toned)" />
|
||||
<MDC :value="feature" unwrap="p" class="text-sm truncate text-(--ui-text-toned)" :cache-key="`pro-pricing-figma-feature-${index}`" />
|
||||
</li>
|
||||
</template>
|
||||
</UPricingPlan>
|
||||
</UContainer>
|
||||
</div>
|
||||
</UPageHero>
|
||||
|
||||
<UPageSection
|
||||
id="testimonials"
|
||||
v-bind="page.testimonials"
|
||||
class="border-y border-(--ui-border)"
|
||||
>
|
||||
<UPageMarquee pause-on-hover :ui="{ root: '[--duration:40s]' }">
|
||||
<img
|
||||
@@ -99,14 +102,16 @@ defineOgImageComponent('Docs', {
|
||||
id="faq"
|
||||
v-bind="page.faq"
|
||||
class="scroll-mt-(--ui-header-height)"
|
||||
:ui="{ container: 'relative' }"
|
||||
>
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
<UPageAccordion
|
||||
multiple
|
||||
:items="(page.faq.items as any[])"
|
||||
class="max-w-4xl mx-auto"
|
||||
>
|
||||
<template #body="{ item }">
|
||||
<MDC :value="item.content" unwrap="p" />
|
||||
<template #body="{ item, index }">
|
||||
<MDC :value="item.content" unwrap="p" :cache-key="`pro-pricing-faq-${index}-content`" />
|
||||
</template>
|
||||
</UPageAccordion>
|
||||
</UPageSection>
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { joinURL } from 'ufo'
|
||||
// @ts-expect-error yaml is not typed
|
||||
import page from '.content/templates.yml'
|
||||
|
||||
const title = page.head?.title || page.title
|
||||
const description = page.head?.description || page.description
|
||||
const { url } = useSiteConfig()
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: `${title} - Nuxt UI Pro`,
|
||||
ogDescription: description
|
||||
})
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: 'Pro'
|
||||
title: page.title,
|
||||
description: page.description,
|
||||
ogTitle: page.title,
|
||||
ogDescription: page.description,
|
||||
ogImage: joinURL(url, '/pro/templates/og-image.png')
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div class="relative">
|
||||
<UPageHero :links="page.links">
|
||||
<template #top>
|
||||
<div class="absolute z-[-1] rounded-full bg-(--ui-primary) blur-[300px] size-60 sm:size-80 transform -translate-x-1/2 left-1/2 -translate-y-80" />
|
||||
<StarsBg />
|
||||
</template>
|
||||
<UPageHero :links="page.links" :ui="{ container: 'relative' }">
|
||||
<StarsBg />
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
|
||||
<template #title>
|
||||
<MDC :value="page.title" unwrap="p" />
|
||||
<MDC :value="page.hero.title" unwrap="p" cache-key="pro-templates-hero-title" />
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
<MDC :value="page.description" unwrap="p" />
|
||||
<MDC :value="page.hero.description" unwrap="p" cache-key="pro-templates-hero-description" />
|
||||
</template>
|
||||
</UPageHero>
|
||||
|
||||
@@ -40,30 +37,39 @@ defineOgImageComponent('Docs', {
|
||||
:links="template.links"
|
||||
:features="template.features"
|
||||
orientation="horizontal"
|
||||
reverse
|
||||
class="*:!pt-0"
|
||||
:ui="{ title: 'lg:text-4xl' }"
|
||||
class="lg:border-t border-(--ui-border)"
|
||||
:ui="{
|
||||
title: 'lg:text-4xl',
|
||||
wrapper: 'lg:py-16 lg:border-r border-(--ui-border) order-last lg:pr-16',
|
||||
container: 'lg:py-0',
|
||||
links: 'gap-x-3'
|
||||
}"
|
||||
>
|
||||
<template #description>
|
||||
<MDC :value="template.description" unwrap="p" />
|
||||
<MDC :value="template.description" unwrap="p" :cache-key="`pro-templates-${index}-description`" />
|
||||
</template>
|
||||
|
||||
<UColorModeImage
|
||||
v-if="template.thumbnail"
|
||||
v-bind="template.thumbnail"
|
||||
class="w-full h-auto rounded-(--ui-radius) border border-(--ui-border)"
|
||||
width="656"
|
||||
height="369"
|
||||
/>
|
||||
<UCarousel
|
||||
v-else-if="template.images"
|
||||
v-slot="{ item }"
|
||||
:items="(template.images as any[])"
|
||||
dots
|
||||
>
|
||||
<NuxtImg v-bind="item" class="w-full h-full object-cover" width="576" height="360" />
|
||||
</UCarousel>
|
||||
<Placeholder v-else class="w-full h-full aspect-video" />
|
||||
<div class="lg:border-x border-(--ui-border) h-full flex items-center lg:bg-(--ui-bg-muted)/20">
|
||||
<Motion class="flex-1" :initial="{ opacity: 0, transform: 'translateY(10px)' }" :in-view="{ opacity: 1, transform: 'translateY(0px)' }" :in-view-options="{ once: true }" :transition="{ duration: 0.5, delay: 0.2 }">
|
||||
<UColorModeImage
|
||||
v-if="template.thumbnail"
|
||||
v-bind="template.thumbnail"
|
||||
class="w-full h-auto border lg:border-y lg:border-x-0 border-(--ui-border) rounded-(--ui-radius) lg:rounded-none"
|
||||
width="656"
|
||||
height="369"
|
||||
loading="lazy"
|
||||
/>
|
||||
<UCarousel
|
||||
v-else-if="template.images"
|
||||
v-slot="{ item }"
|
||||
:items="(template.images as any[])"
|
||||
dots
|
||||
>
|
||||
<NuxtImg v-bind="item" class="w-full h-full object-cover" width="576" height="360" />
|
||||
</UCarousel>
|
||||
<Placeholder v-else class="w-full h-full aspect-video" />
|
||||
</Motion>
|
||||
</div>
|
||||
</UPageSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { joinURL } from 'ufo'
|
||||
|
||||
const { data: page } = await useAsyncData('terms', () => queryCollection('content').path('/pro/terms').first())
|
||||
if (!page.value) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||
}
|
||||
|
||||
const { url } = useSiteConfig()
|
||||
|
||||
const title = page.value.title
|
||||
const description = page.value.description
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: `${title} - Nuxt UI Pro`,
|
||||
ogDescription: description
|
||||
})
|
||||
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: 'Pro'
|
||||
ogDescription: description,
|
||||
ogImage: joinURL(url, '/pro/og-image.png')
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ useSeoMeta({
|
||||
description
|
||||
})
|
||||
|
||||
defineOgImageComponent('Docs')
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: 'Community'
|
||||
})
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
@@ -16,12 +16,12 @@ function handleMessage(message) {
|
||||
async function handleFormatMessage(message) {
|
||||
if (!globalThis.prettier) {
|
||||
await Promise.all([
|
||||
import('https://unpkg.com/prettier@3.5.0/standalone.js'),
|
||||
import('https://unpkg.com/prettier@3.5.0/plugins/babel.js'),
|
||||
import('https://unpkg.com/prettier@3.5.0/plugins/estree.js'),
|
||||
import('https://unpkg.com/prettier@3.5.0/plugins/html.js'),
|
||||
import('https://unpkg.com/prettier@3.5.0/plugins/markdown.js'),
|
||||
import('https://unpkg.com/prettier@3.5.0/plugins/typescript.js')
|
||||
import('https://unpkg.com/prettier@3.5.2/standalone.js'),
|
||||
import('https://unpkg.com/prettier@3.5.2/plugins/babel.js'),
|
||||
import('https://unpkg.com/prettier@3.5.2/plugins/estree.js'),
|
||||
import('https://unpkg.com/prettier@3.5.2/plugins/html.js'),
|
||||
import('https://unpkg.com/prettier@3.5.2/plugins/markdown.js'),
|
||||
import('https://unpkg.com/prettier@3.5.2/plugins/typescript.js')
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineCollection, z } from '@nuxt/content'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const schema = z.object({
|
||||
category: z.enum(['layout', 'form', 'element', 'navigation', 'data', 'overlay']).optional(),
|
||||
framework: z.string().optional(),
|
||||
module: z.string().optional(),
|
||||
navigation: z.object({
|
||||
|
||||
@@ -9,9 +9,9 @@ We're thrilled to introduce this major update to our UI library, bringing signif
|
||||
|
||||
## What's New in v3?
|
||||
|
||||
<iframe width="100%" height="100%" src="https://www.youtube-nocookie.com/embed/_eQxomah-nA?si=pDSzchUBDKb2NQu7" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen style="aspect-ratio: 16/9;"></iframe>
|
||||
<iframe width="100%" height="100%" src="https://www.youtube-nocookie.com/embed/_eQxomah-nA?si=pDSzchUBDKb2NQu7" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen style="aspect-ratio: 16/9;" class="rounded-[calc(var(--ui-radius)*1.5)]"></iframe>
|
||||
|
||||
### Reka UI (Radix Vue)
|
||||
### Reka UI
|
||||
|
||||
We've transitioned from [Headless UI](https://headlessui.com/) to [Reka UI](https://reka-ui.com/) as our core component foundation. This shift brings several key advantages:
|
||||
|
||||
@@ -31,8 +31,8 @@ Nuxt UI v3 integrates the latest Tailwind CSS v4, bringing significant improveme
|
||||
- **CSS-first configuration**: A reimagined developer experience where you customize and extend the framework directly in CSS instead of a JavaScript configuration file.
|
||||
- **Designed for the modern web**: Built on native cascade layers, wide-gamut colors, and including first-class support for modern CSS features like container queries, @starting-style, popovers, and more.
|
||||
|
||||
::note{to="https://tailwindcss.com/docs/upgrade-guide" target="_blank" aria-label="Tailwind CSS v4 upgrade guide"}
|
||||
Learn how to upgrade your project from Tailwind CSS v3 to v4.
|
||||
::note{to="https://tailwindcss.com/docs/upgrade-guide#changes-from-v3" target="_blank" aria-label="Tailwind CSS v4 upgrade guide"}
|
||||
Learn about all the breaking changes in Tailwind CSS v4.
|
||||
::
|
||||
|
||||
### Tailwind Variants
|
||||
@@ -79,16 +79,24 @@ Learn how to install and configure Nuxt UI in a Vue project in the **Vue install
|
||||
|
||||
### Nuxt DevTools Integration
|
||||
|
||||
Nuxt UI is deeply integrated with Nuxt Devtools, providing a powerful development experience:
|
||||
You can play with Nuxt UI components as well as your app components directly from Nuxt Devtools with the [compodium](https://github.com/romhml/compodium) module, providing a powerful development experience:
|
||||
|
||||
- **Component Inspector**: Inspect and analyze Nuxt UI components in real-time
|
||||
- **Live Preview**: Modify component props and see changes instantly
|
||||
- **Code Generation**: Get the corresponding code for your component configurations
|
||||
|
||||
::video{poster="https://res.cloudinary.com/nuxt/video/upload/so_0/v1736788078/nuxt-ui/nuxt-ui3-devtools_wbmgmc.jpg" controls class="w-full h-auto rounded"}
|
||||
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1736788078/nuxt-ui/nuxt-ui3-devtools_wbmgmc.webm" type="video/webm"}
|
||||
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1736788078/nuxt-ui/nuxt-ui3-devtools_wbmgmc.mp4" type="video/mp4"}
|
||||
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1736788078/nuxt-ui/nuxt-ui3-devtools_wbmgmc.ogg" type="video/ogg"}
|
||||
::note
|
||||
Install the module to your Nuxt application with one command:
|
||||
|
||||
```bash [Terminal]
|
||||
npx nuxi module add compodium
|
||||
```
|
||||
::
|
||||
|
||||
::video{poster="https://res.cloudinary.com/nuxt/video/upload/so_0/v1740751953/nuxt-ui/nuxt-compodium_y2bvqw.jpg" controls class="w-full h-auto rounded"}
|
||||
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1740751953/nuxt-ui/nuxt-compodium_y2bvqw.webm" type="video/webm"}
|
||||
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1740751953/nuxt-ui/nuxt-compodium_y2bvqw.mp4" type="video/mp4"}
|
||||
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1740751953/nuxt-ui/nuxt-compodium_y2bvqw.ogg" type="video/ogg"}
|
||||
::
|
||||
|
||||
## Migration
|
||||
@@ -97,7 +105,7 @@ We want to be transparent: migrating from Nuxt UI v2 to v3 will require signific
|
||||
|
||||
Key points to consider:
|
||||
|
||||
- A comprehensive migration guide will be available in the coming weeks.
|
||||
- Read our [migration guide](/getting-started/migration) to upgrade your project from v2 to v3.
|
||||
- Review the new documentation and components carefully before attempting to upgrade.
|
||||
- If you encounter any issues, please report them on our [GitHub repository](https://github.com/nuxt/ui/issues).
|
||||
|
||||
@@ -113,7 +121,7 @@ Key points to consider:
|
||||
::
|
||||
|
||||
::accordion-item{label="What about Nuxt UI Pro?"}
|
||||
We've also rebuilt Nuxt UI Pro from scratch and released a `v3.0.0-alpha.x` package with almost all components ready. This is a free update, so the license you buy now is valid for all UI Pro versions. We're actively working to finish the rewrite of all Nuxt UI Pro components.
|
||||
We've also rebuilt Nuxt UI Pro from scratch as v3 to match Nuxt UI version. The license you bought or will buy is valid for both Nuxt UI Pro v1 and v3, this is a **free update**. You can follow the [installation guide](/getting-started/installation/pro/nuxt) to get started.
|
||||
::
|
||||
|
||||
::accordion-item{label="Will Nuxt UI v3 work with other CSS frameworks like UnoCSS?"}
|
||||
@@ -129,7 +137,7 @@ Key points to consider:
|
||||
::
|
||||
|
||||
::accordion-item{label="Is this version stable and suitable for production use?"}
|
||||
As Nuxt UI v3 is currently in alpha, we recommend thorough testing before using it in production environments. We're actively working on stabilization and welcome feedback from early adopters to improve the library. Feel free to report any issues you encounter on our [GitHub repository](https://github.com/nuxt/ui/issues).
|
||||
Nuxt UI v3 is now in beta and is stable enough to be used in production. We now recommend using v3 over v2. We welcome feedback from users to help improve the library further. Feel free to report any issues you encounter on our [GitHub repository](https://github.com/nuxt/ui/issues).
|
||||
::
|
||||
::
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Looking for the **Vue** version?
|
||||
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the Nuxt UI v3 alpha package
|
||||
#### Install the Nuxt UI v3 beta package
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
@@ -43,7 +43,7 @@ bun add @nuxt/ui@next
|
||||
::
|
||||
|
||||
::warning
|
||||
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss@next` in your project's root directory.
|
||||
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss` in your project's root directory.
|
||||
::
|
||||
|
||||
#### Add the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}
|
||||
@@ -56,15 +56,14 @@ export default defineNuxtConfig({
|
||||
|
||||
#### Import Tailwind CSS and Nuxt UI in your CSS
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
::code-group
|
||||
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
```
|
||||
|
||||
::tip
|
||||
Use the `css` property in your `nuxt.config.ts` to import your CSS file.
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
```ts [nuxt.config.ts] {3}
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui'],
|
||||
css: ['~/assets/css/main.css']
|
||||
@@ -73,6 +72,10 @@ export default defineNuxtConfig({
|
||||
|
||||
::
|
||||
|
||||
::warning
|
||||
The `theme(static)` is required since [`tailwindcss@4.0.8`](https://github.com/tailwindlabs/tailwindcss/releases/tag/v4.0.8) introduced a breaking change to only expose used CSS variables.
|
||||
::
|
||||
|
||||
::callout{icon="i-simple-icons-visualstudiocode"}
|
||||
It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension for VSCode and add the following settings:
|
||||
|
||||
@@ -98,7 +101,7 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
|
||||
```
|
||||
|
||||
::note{to="/components/app"}
|
||||
The `App` component provides global configurations and is required for **Toast** and **Tooltip** components to work.
|
||||
The `App` component provides global configurations and is required for **Toast**, **Tooltip** components to work as well as **Programmatic Overlays**.
|
||||
::
|
||||
|
||||
::
|
||||
@@ -137,6 +140,7 @@ Use the `prefix` option to change the prefix of the components.
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui'],
|
||||
css: ['~/assets/css/main.css'],
|
||||
ui: {
|
||||
prefix: 'Nuxt'
|
||||
}
|
||||
@@ -152,6 +156,7 @@ Use the `fonts` option to enable or disable the [`@nuxt/fonts`](https://github.c
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui'],
|
||||
css: ['~/assets/css/main.css'],
|
||||
ui: {
|
||||
fonts: false
|
||||
}
|
||||
@@ -167,6 +172,7 @@ Use the `colorMode` option to enable or disable the [`@nuxt/color-mode`](https:/
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui'],
|
||||
css: ['~/assets/css/main.css'],
|
||||
ui: {
|
||||
colorMode: false
|
||||
}
|
||||
@@ -182,6 +188,7 @@ Use the `theme.colors` option to define the dynamic color aliases used to genera
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui'],
|
||||
css: ['~/assets/css/main.css'],
|
||||
ui: {
|
||||
theme: {
|
||||
colors: ['primary', 'error']
|
||||
@@ -203,6 +210,7 @@ Use the `theme.transitions` option to enable or disable transitions on component
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui'],
|
||||
css: ['~/assets/css/main.css'],
|
||||
ui: {
|
||||
theme: {
|
||||
transitions: false
|
||||
@@ -215,23 +223,6 @@ export default defineNuxtConfig({
|
||||
This option adds the `transition-colors` class on components with hover or active states.
|
||||
::
|
||||
|
||||
### `devtools.enabled`
|
||||
|
||||
Use the `devtools.enabled` option to enable or disable the Nuxt UI devtools.
|
||||
|
||||
- Default: `true`{lang="ts-type"}
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui'],
|
||||
ui: {
|
||||
devtools: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Continuous Releases
|
||||
|
||||
Nuxt UI v3 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.
|
||||
|
||||
@@ -20,7 +20,7 @@ Looking for the **Nuxt** version?
|
||||
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the Nuxt UI v3 alpha package
|
||||
#### Install the Nuxt UI v3 beta package
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
@@ -43,7 +43,7 @@ bun add @nuxt/ui@next
|
||||
::
|
||||
|
||||
::warning
|
||||
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss@next`, `vue-router` and `@unhead/vue` in your project's root directory.
|
||||
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss`, `vue-router` and `@unhead/vue` in your project's root directory.
|
||||
::
|
||||
|
||||
#### Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}
|
||||
@@ -102,10 +102,14 @@ app.mount('#app')
|
||||
#### Import Tailwind CSS and Nuxt UI in your CSS
|
||||
|
||||
```css [assets/main.css]
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
```
|
||||
|
||||
::warning
|
||||
The `theme(static)` is required since [`tailwindcss@4.0.8`](https://github.com/tailwindlabs/tailwindcss/releases/tag/v4.0.8) introduced a breaking change to only expose used CSS variables.
|
||||
::
|
||||
|
||||
::tip
|
||||
Import the CSS file in your `main.ts`.
|
||||
|
||||
@@ -157,19 +161,19 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
|
||||
```
|
||||
|
||||
::note{to="/components/app"}
|
||||
The `App` component provides global configurations and is required for **Toast** and **Tooltip** components to work.
|
||||
The `App` component provides global configurations and is required for **Toast**, **Tooltip** components to work as well as **Programmatic Overlays**.
|
||||
::
|
||||
|
||||
::
|
||||
|
||||
### Use our Vue starter
|
||||
|
||||
Start your project using the [nuxtlabs/nuxt-ui3-vue-starter](https://github.com/nuxtlabs/nuxt-ui3-vue-starter) template with Nuxt UI v3 pre-configured.
|
||||
Start your project using the [nuxtlabs/nuxt-ui-vue-starter](https://github.com/nuxtlabs/nuxt-ui-vue-starter) template with Nuxt UI v3 pre-configured.
|
||||
|
||||
Create a new project locally by running the following command:
|
||||
|
||||
```bash [Terminal]
|
||||
npx nuxi init -t github:nuxtlabs/nuxt-ui3-vue-starter <my-app>
|
||||
npx nuxi init -t github:nuxtlabs/nuxt-ui-vue-starter <my-app>
|
||||
```
|
||||
|
||||
::note
|
||||
|
||||
457
docs/content/1.getting-started/2.migration.md
Normal file
457
docs/content/1.getting-started/2.migration.md
Normal file
@@ -0,0 +1,457 @@
|
||||
---
|
||||
title: Migration
|
||||
description: 'A comprehensive guide to migrate your application from Nuxt UI v2 to Nuxt UI v3.'
|
||||
---
|
||||
|
||||
Nuxt UI v3.0 is a new major version rebuilt from the ground up, introducing a modern architecture with significant performance improvements and an enhanced developer experience. This major release includes several breaking changes alongside powerful new features and capabilities:
|
||||
|
||||
- **Tailwind CSS v4**: Migration from JavaScript to CSS-based configuration
|
||||
- **Reka UI**: Replacing Headless UI as the underlying component library
|
||||
- **Tailwind Variants**: New styling API for component variants
|
||||
|
||||
This guide provides step by step instructions to migrate your application from v2 to v3.
|
||||
|
||||
## Migrate your project
|
||||
|
||||
::steps
|
||||
|
||||
### Update Tailwind CSS
|
||||
|
||||
Tailwind CSS v4 introduces significant changes to its configuration approach. The official Tailwind upgrade tool will help automate most of the migration process.
|
||||
|
||||
::note{to="https://tailwindcss.com/docs/upgrade-guide#changes-from-v3" target="_blank"}
|
||||
For a detailed walkthrough of all changes, refer to the official **Tailwind CSS v4 upgrade guide**.
|
||||
::
|
||||
|
||||
1. Create a `main.css` file and import it in your `nuxt.config.ts` file:
|
||||
|
||||
::code-group
|
||||
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
```
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
css: ['~/assets/css/main.css']
|
||||
})
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
2. Run the Tailwind CSS upgrade tool:
|
||||
|
||||
```bash
|
||||
npx @tailwindcss/upgrade
|
||||
```
|
||||
|
||||
### Update Nuxt UI
|
||||
|
||||
3. Install the latest version of the package:
|
||||
|
||||
::module-only
|
||||
#ui
|
||||
:::div
|
||||
|
||||
::::code-group{sync="pm"}
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add @nuxt/ui@next
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add @nuxt/ui@next
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install @nuxt/ui@next
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add @nuxt/ui@next
|
||||
```
|
||||
|
||||
::::
|
||||
|
||||
:::
|
||||
|
||||
#ui-pro
|
||||
:::div
|
||||
|
||||
::::code-group{sync="pm"}
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add @nuxt/ui-pro@next
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add @nuxt/ui-pro@next
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install @nuxt/ui-pro@next
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add @nuxt/ui-pro@next
|
||||
```
|
||||
|
||||
::::
|
||||
|
||||
:::
|
||||
|
||||
::
|
||||
|
||||
4. Import it in your CSS:
|
||||
|
||||
::module-only
|
||||
#ui
|
||||
:::div
|
||||
|
||||
```css [app/assets/css/main.css]{2}
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
#ui-pro
|
||||
:::div
|
||||
|
||||
```css [app/assets/css/main.css]{2}
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui-pro";
|
||||
```
|
||||
|
||||
:::
|
||||
::
|
||||
|
||||
::module-only
|
||||
#ui
|
||||
|
||||
:::div
|
||||
5. Wrap you app with the [App](/components/app) component:
|
||||
:::
|
||||
|
||||
#ui-pro
|
||||
:::div
|
||||
5. Add the `@nuxt/ui-pro` module in your `nuxt.config.ts` file as it's no longer a layer:
|
||||
|
||||
```diff [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
- extends: ['@nuxt/ui-pro'],
|
||||
- modules: ['@nuxt/ui']
|
||||
+ modules: ['@nuxt/ui-pro']
|
||||
})
|
||||
```
|
||||
|
||||
6. Wrap you app with the [App](/components/app) component:
|
||||
:::
|
||||
|
||||
::
|
||||
|
||||
```vue [app.vue] {2,4}
|
||||
<template>
|
||||
<UApp>
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
## Changes from v2
|
||||
|
||||
Now that you have updated your project, you can start migrating your code. Here's a comprehensive list of all the breaking changes in Nuxt UI v3.
|
||||
|
||||
### Updated design system
|
||||
|
||||
In Nuxt UI v2, we had a mix between a design system with `primary`, `gray`, `error` aliases and all the colors from Tailwind CSS. We've replaced it with a proper [design system](/getting-started/theme#design-system) with 7 color aliases:
|
||||
|
||||
| Color | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `primary`{color="primary"} | `green` | Main brand color, used as the default color for components. |
|
||||
| `secondary`{color="secondary"} | `blue` | Secondary color to complement the primary color. |
|
||||
| `success`{color="success"} | `green` | Used for success states. |
|
||||
| `info`{color="info"} | `blue` | Used for informational states. |
|
||||
| `warning`{color="warning"} | `yellow` | Used for warning states. |
|
||||
| `error`{color="error"} | `red` | Used for form error validation states. |
|
||||
| `neutral` | `slate` | Neutral color for backgrounds, text, etc. |
|
||||
|
||||
This change introduces several breaking changes that you need to be aware of:
|
||||
|
||||
1. The `gray` color has been renamed to `neutral`
|
||||
|
||||
```diff
|
||||
<template>
|
||||
- <p class="text-gray-500 dark:text-gray-400" />
|
||||
+ <p class="text-neutral-500 dark:text-neutral-400" />
|
||||
</template>
|
||||
```
|
||||
|
||||
::note
|
||||
You can also use the new [design tokens](/getting-started/theme#neutral-palette) to handle light and dark mode:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
- <p class="text-gray-500 dark:text-gray-400" />
|
||||
+ <p class="text-(--ui-text-muted)" />
|
||||
|
||||
- <p class="text-gray-900 dark:text-white" />
|
||||
+ <p class="text-(--ui-text-highlighted)" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
2. The `DEFAULT` shade that let you write `text-primary` no longer exists, you can use [color shades](/getting-started/theme#color-shades) instead:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
- <p class="text-primary">Hello</p>
|
||||
+ <p class="text-(--ui-primary)">Hello</p>
|
||||
</template>
|
||||
```
|
||||
|
||||
3. The `gray`, `black` and `white` in the `color` props have been removed in favor of `neutral`:
|
||||
|
||||
```diff
|
||||
- <UButton color="black" />
|
||||
+ <UButton color="neutral" />
|
||||
|
||||
- <UButton color="gray" />
|
||||
+ <UButton color="neutral" variant="subtle" />
|
||||
|
||||
- <UButton color="white" />
|
||||
+ <UButton color="neutral" variant="outline" />
|
||||
```
|
||||
|
||||
4. You can no longer use Tailwind CSS colors in the `color` props, use the new aliases instead:
|
||||
|
||||
```diff
|
||||
- <UButton color="red" />
|
||||
+ <UButton color="error" />
|
||||
```
|
||||
|
||||
::note{to="/getting-started/theme#colors"}
|
||||
Learn how to extend the design system to add new color aliases.
|
||||
::
|
||||
|
||||
5. The color configuration in `app.config.ts` has been moved into a `colors` object:
|
||||
|
||||
```diff
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
- primary: 'green',
|
||||
- gray: 'cool'
|
||||
+ colors: {
|
||||
+ primary: 'green',
|
||||
+ neutral: 'slate'
|
||||
+ }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Updated theming system
|
||||
|
||||
Nuxt UI components are now styled using the [Tailwind Variants API](/getting-started/theme#components-theme), which makes all the overrides you made using the `app.config.ts` and the `ui` prop obsolete.
|
||||
|
||||
1. Update your [`app.config.ts`](/getting-started/theme#config) to override components with their new theme:
|
||||
|
||||
```diff
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
button: {
|
||||
- font: 'font-bold',
|
||||
- default: {
|
||||
- size: 'md',
|
||||
- color: 'primary'
|
||||
- }
|
||||
+ slots: {
|
||||
+ base: 'font-medium'
|
||||
+ },
|
||||
+ defaultVariants: {
|
||||
+ size: 'md',
|
||||
+ color: 'primary'
|
||||
+ }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
2. Update your [`ui` props](/getting-started/theme#props) to override each component's slots using their new theme:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
- <UButton :ui="{ font: 'font-bold' }" />
|
||||
+ <UButton :ui="{ base: 'font-bold' }" />
|
||||
</template>
|
||||
```
|
||||
|
||||
::tip{to="/components/button#theme"}
|
||||
We can't detail all the changes here but you can check each component's theme in the **Theme** section.
|
||||
::
|
||||
|
||||
### Renamed components
|
||||
|
||||
We've renamed some Nuxt UI components to align with the Reka UI naming convention:
|
||||
|
||||
| v2 | v3 |
|
||||
| --- | --- |
|
||||
| `Divider` | [`Separator`](/components/separator) |
|
||||
| `Dropdown` | [`DropdownMenu`](/components/dropdown-menu) |
|
||||
| `FormGroup` | [`FormField`](/components/form-field) |
|
||||
| `Range` | [`Slider`](/components/slider) |
|
||||
| `Toggle` | [`Switch`](/components/switch) |
|
||||
| `Notification` | [`Toast`](/components/toast) |
|
||||
| `VerticalNavigation` | [`NavigationMenu`](/components/navigation-menu) with `orientation="vertical"` |
|
||||
| `HorizontalNavigation` | [`NavigationMenu`](/components/navigation-menu) with `orientation="horizontal"` |
|
||||
|
||||
::module-only
|
||||
#ui-pro
|
||||
:::div
|
||||
Here are the Nuxt UI Pro components that have been renamed or removed:
|
||||
|
||||
| v1 | v3 |
|
||||
| --- | --- |
|
||||
| `BlogList` | [`BlogPosts`](/components/blog-posts) |
|
||||
| `ColorModeToggle` | [`ColorModeSwitch`](/components/color-mode-switch) |
|
||||
| `DashboardCard` | Removed (use [`PageCard`](/components/page-card) instead) |
|
||||
| `DashboardLayout` | [`DashboardGroup`](/components/dashboard-group) |
|
||||
| `DashboardModal` | Removed (use [`Modal`](/components/modal) instead) |
|
||||
| `DashboardNavbarToggle` | [`DashboardSidebarToggle`](/components/dashboard-sidebar-toggle) |
|
||||
| `DashboardPage` | Removed |
|
||||
| `DashboardPanelContent` | Removed (use `#body` slot instead) |
|
||||
| `DashboardPanelHandle` | [`DashboardResizeHandle`](/components/dashboard-resize-handle) |
|
||||
| `DashboardSection` | Removed (use [`PageCard`](/components/page-card) instead) |
|
||||
| `DashboardSidebarLinks` | Removed (use [`NavigationMenu`](/components/navigation-menu) instead) |
|
||||
| `DashboardSlideover` | Removed (use [`Slideover`](/components/slideover) instead) |
|
||||
| `FooterLinks` | Removed (use [`NavigationMenu`](/components/navigation-menu) instead) |
|
||||
| `HeaderLinks` | Removed (use [`NavigationMenu`](/components/navigation-menu) instead) |
|
||||
| `LandingCard` | Removed (use [`PageCard`](/components/page-card) instead) |
|
||||
| `LandingCTA` | [`PageCTA`](/components/page-cta) |
|
||||
| `LandingFAQ` | Removed (use [`PageAccordion`](/components/page-accordion) instead) |
|
||||
| `LandingGrid` | Removed (use [`PageGrid`](/components/page-grid) instead) |
|
||||
| `LandingHero` | Removed (use [`PageHero`](/components/page-hero) instead) |
|
||||
| `LandingLogos` | [`PageLogos`](/components/page-logos) |
|
||||
| `LandingSection` | [`PageSection`](/components/page-section) |
|
||||
| `LandingTestimonial` | Removed (use [`PageCard`](/components/page-card#as-a-testimonial) instead) |
|
||||
| `NavigationAccordion` | [`ContentNavigation`](/components/content-navigation) |
|
||||
| `NavigationLinks` | [`ContentNavigation`](/components/content-navigation) |
|
||||
| `NavigationTree` | [`ContentNavigation`](/components/content-navigation) |
|
||||
| `PageError` | [`Error`](/components/error) |
|
||||
| `PricingCard` | [`PricingPlan`](/components/pricing-plan) |
|
||||
| `PricingGrid` | [`PricingPlans`](/components/pricing-plans) |
|
||||
| `PricingSwitch` | Removed (use [`Switch`](/components/switch) or [`Tabs`](/components/tabs) instead) |
|
||||
|
||||
:::
|
||||
|
||||
::
|
||||
|
||||
### Changed components
|
||||
|
||||
In addition to the renamed components, there are lots of changes to the components API. Let's detail the most important ones:
|
||||
|
||||
1. The `links` and `options` props have been renamed to `items` for consistency:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
- <USelect :options="countries" />
|
||||
+ <USelect :items="countries" />
|
||||
|
||||
- <UHorizontalNavigation :links="links" />
|
||||
+ <UNavigationMenu :items="links" />
|
||||
</template>
|
||||
```
|
||||
|
||||
::note
|
||||
This change affects the following components: `Breadcrumb`, `HorizontalNavigation`, `InputMenu`, `RadioGroup`, `Select`, `SelectMenu`, `VerticalNavigation`.
|
||||
::
|
||||
|
||||
2. The global `Modals`, `Slideovers` and `Notifications` components have been removed in favor the [App](/components/app) component:
|
||||
|
||||
```diff [app.vue]
|
||||
<template>
|
||||
+ <UApp>
|
||||
+ <NuxtPage />
|
||||
+ </UApp>
|
||||
- <UModals />
|
||||
- <USlideovers />
|
||||
- <UNotifications />
|
||||
</template>
|
||||
```
|
||||
|
||||
3. The `v-model:open` directive and `default-open` prop are now used to control visibility:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
- <UModal v-model="open" />
|
||||
+ <UModal v-model:open="open" />
|
||||
</template>
|
||||
```
|
||||
|
||||
::note
|
||||
This change affects the following components: `ContextMenu`, `Modal` and `Slideover` and enables controlling visibility for `InputMenu`, `Select`, `SelectMenu` and `Tooltip`.
|
||||
::
|
||||
|
||||
4. The default slot is now used for the trigger and the content goes inside the `#content` slot (you don't need to use a `v-model:open` directive with this method):
|
||||
|
||||
```diff
|
||||
<script setup lang="ts">
|
||||
- const open = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
- <UButton label="Open" @click="open = true" />
|
||||
|
||||
- <UModal v-model="open">
|
||||
+ <UModal>
|
||||
+ <UButton label="Open" />
|
||||
|
||||
+ <template #content>
|
||||
<div class="p-4">
|
||||
<Placeholder class="h-48" />
|
||||
</div>
|
||||
+ </template>
|
||||
</UModal>
|
||||
</template>
|
||||
```
|
||||
|
||||
::note
|
||||
This change affects the following components: `Modal`, `Popover`, `Slideover`, `Tooltip`.
|
||||
::
|
||||
|
||||
5. A `#header`, `#body` and `#footer` slots have been added inside the `#content` slot like the `Card` component:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
- <UModal>
|
||||
+ <UModal title="Title" description="Description">
|
||||
- <div class="p-4">
|
||||
+ <template #body>
|
||||
<Placeholder class="h-48" />
|
||||
+ </template>
|
||||
- </div>
|
||||
</UModal>
|
||||
</template>
|
||||
```
|
||||
|
||||
::note
|
||||
This change affects the following components: `Modal`, `Slideover`.
|
||||
::
|
||||
|
||||
6. The `Toast` component `timeout` prop has been renamed to `duration`:
|
||||
|
||||
```diff
|
||||
<script setup lang="ts">
|
||||
const toast = useToast()
|
||||
|
||||
- toast.add({ title: 'Invitation sent', timeout: 0 })
|
||||
+ toast.add({ title: 'Invitation sent', duration: 0 })
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
::warning
|
||||
This page is a work in progress, we'll improve it regularly.
|
||||
::
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: Theme
|
||||
description: 'Learn how to customize Nuxt UI components using Tailwind CSS v4, CSS variables and the Tailwind Variants API for powerful and flexible theming.'
|
||||
navigation.icon: i-lucide-palette
|
||||
navigation.icon: i-lucide-swatch-book
|
||||
---
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
Nuxt UI v3 uses Tailwind CSS v4, you can read the [upgrade guide](https://tailwindcss.com/docs/upgrade-guide) to learn how to upgrade your project from v3 to v4.
|
||||
Nuxt UI v3 uses Tailwind CSS v4, you can read the official [upgrade guide](https://tailwindcss.com/docs/upgrade-guide#changes-from-v3) to learn about all the breaking changes.
|
||||
|
||||
### `@theme`
|
||||
|
||||
@@ -16,11 +16,11 @@ Tailwind CSS v4 takes a CSS-first configuration approach, you now customize your
|
||||
#ui
|
||||
:::div
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme {
|
||||
@theme static {
|
||||
--font-sans: 'Public Sans', sans-serif;
|
||||
|
||||
--breakpoint-3xl: 1920px;
|
||||
@@ -44,11 +44,11 @@ Tailwind CSS v4 takes a CSS-first configuration approach, you now customize your
|
||||
#ui-pro
|
||||
:::div
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui-pro";
|
||||
|
||||
@theme {
|
||||
@theme static {
|
||||
--font-sans: 'Public Sans', sans-serif;
|
||||
|
||||
--breakpoint-3xl: 1920px;
|
||||
@@ -80,17 +80,19 @@ Learn more about customizing your theme in the theme variables documentation.
|
||||
|
||||
You can use the [`@source` directive](https://tailwindcss.com/docs/functions-and-directives#source-directive) to explicitly specify source files that aren't picked up by Tailwind's automatic content detection:
|
||||
|
||||
This can be useful when writing Tailwind classes in markdown files with [`@nuxt/content`](https://github.com/nuxt/content) for example:
|
||||
This can be useful when writing Tailwind CSS classes in markdown files with [`@nuxt/content`](https://github.com/nuxt/content) for example:
|
||||
|
||||
::module-only
|
||||
#ui
|
||||
:::div
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@source "../../content"; /* Change to "../../../content" if using compatibilityVersion: 4 in nuxt.config.ts */
|
||||
@source "../../../content";
|
||||
/* Use this if you're not using compatibilityVersion: 4: https://nuxt.com/docs/getting-started/upgrade#opting-in-to-nuxt-4 */
|
||||
@source "../../content";
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -98,11 +100,13 @@ This can be useful when writing Tailwind classes in markdown files with [`@nuxt/
|
||||
#ui-pro
|
||||
:::div
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui-pro";
|
||||
|
||||
@source "../../content"; /* Change to "../../../content" if using compatibilityVersion: 4 in nuxt.config.ts */
|
||||
@source "../../../content";
|
||||
/* Use this if you're not using compatibilityVersion: 4: https://nuxt.com/docs/getting-started/upgrade#opting-in-to-nuxt-4 */
|
||||
@source "../../content";
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -138,7 +142,7 @@ Nuxt UI leverages Vite config to provide customizable color aliases based on [Ta
|
||||
|
||||
::framework-only
|
||||
#nuxt
|
||||
::div
|
||||
:::div
|
||||
You can configure these color aliases at runtime in your `app.config.ts` file under the `ui.colors` key, allowing for dynamic theme customization without requiring an application rebuild:
|
||||
|
||||
```ts [app.config.ts]
|
||||
@@ -152,12 +156,19 @@ export default defineAppConfig({
|
||||
})
|
||||
```
|
||||
|
||||
::
|
||||
:::
|
||||
|
||||
#vue
|
||||
::div
|
||||
|
||||
:::div
|
||||
You can configure these color aliases at runtime in your `vite.config.ts` file under the `ui.colors` key:
|
||||
|
||||
::::module-only
|
||||
|
||||
#ui
|
||||
|
||||
:::::div
|
||||
|
||||
```ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
@@ -177,8 +188,37 @@ export default defineConfig({
|
||||
]
|
||||
})
|
||||
```
|
||||
:::::
|
||||
|
||||
::
|
||||
#ui-pro
|
||||
|
||||
:::::div
|
||||
|
||||
```ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import uiPro from '@nuxt/ui-pro/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
uiPro({
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'blue',
|
||||
neutral: 'zinc'
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
:::::
|
||||
|
||||
::::
|
||||
|
||||
:::
|
||||
|
||||
::
|
||||
|
||||
@@ -225,9 +265,17 @@ export default defineNuxtConfig({
|
||||
:::
|
||||
|
||||
#vue
|
||||
|
||||
:::tip
|
||||
|
||||
You can add you own dynamic color aliases in your `vite.config.ts`, you just have to make sure to also define them in the [`theme.colors`](/getting-started/installation/vue#themecolors) option of the `ui` plugin.
|
||||
|
||||
::::module-only
|
||||
|
||||
#ui
|
||||
|
||||
:::::div
|
||||
|
||||
```ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
@@ -250,7 +298,40 @@ export default defineConfig({
|
||||
})
|
||||
```
|
||||
|
||||
:::::
|
||||
|
||||
#ui-pro
|
||||
|
||||
:::::div
|
||||
|
||||
```ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import uiPro from '@nuxt/ui-pro/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
uiPro({
|
||||
ui: {
|
||||
colors: {
|
||||
tertiary: 'indigo'
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error']
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
:::::
|
||||
|
||||
::::
|
||||
|
||||
:::
|
||||
|
||||
::
|
||||
|
||||
### Tokens
|
||||
@@ -298,8 +379,8 @@ You can change which shade is used for each color on light and dark mode:
|
||||
#ui
|
||||
:::div{class="*:!mb-0 *:!mt-2.5"}
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
|
||||
:root {
|
||||
@@ -316,8 +397,8 @@ You can change which shade is used for each color on light and dark mode:
|
||||
#ui-pro
|
||||
:::div{class="*:!mb-0 *:!mt-2.5"}
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui-pro";
|
||||
|
||||
:root {
|
||||
@@ -352,8 +433,8 @@ You cannot set `primary: 'black'`{lang="ts-type"} in your [`vite.config.ts`](#co
|
||||
#ui
|
||||
:::div{class="*:!mb-0 *:!mt-2.5"}
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
|
||||
:root {
|
||||
@@ -370,8 +451,8 @@ You cannot set `primary: 'black'`{lang="ts-type"} in your [`vite.config.ts`](#co
|
||||
#ui-pro
|
||||
:::div{class="*:!mb-0 *:!mt-2.5"}
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui-pro";
|
||||
|
||||
:root {
|
||||
@@ -482,8 +563,8 @@ You can customize these CSS variables to tailor the appearance of your applicati
|
||||
#ui
|
||||
:::div{class="*:!mb-0 *:!mt-2.5"}
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
|
||||
:root {
|
||||
@@ -502,8 +583,8 @@ You can customize these CSS variables to tailor the appearance of your applicati
|
||||
#ui-pro
|
||||
:::div{class="*:!mb-0 *:!mt-2.5"}
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui-pro";
|
||||
|
||||
:root {
|
||||
@@ -543,8 +624,8 @@ You can customize the default radius value using the default Tailwind CSS variab
|
||||
#ui
|
||||
:::div{class="*:!mb-0 *:!mt-2.5"}
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
|
||||
:root {
|
||||
@@ -557,8 +638,8 @@ You can customize the default radius value using the default Tailwind CSS variab
|
||||
#ui-pro
|
||||
:::div{class="*:!mb-0 *:!mt-2.5"}
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui-pro";
|
||||
|
||||
:root {
|
||||
@@ -588,8 +669,8 @@ You can customize the default container width using the default Tailwind CSS var
|
||||
#ui
|
||||
:::div{class="*:!mb-0 *:!mt-2.5"}
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme {
|
||||
@@ -606,8 +687,8 @@ You can customize the default container width using the default Tailwind CSS var
|
||||
#ui-pro
|
||||
:::div{class="*:!mb-0 *:!mt-2.5"}
|
||||
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
```css [app/assets/css/main.css]
|
||||
@import "tailwindcss" theme(static);
|
||||
@import "@nuxt/ui-pro";
|
||||
|
||||
@theme {
|
||||
@@ -757,14 +838,14 @@ You can explore the theme for each component in two ways:
|
||||
|
||||
- Check the `Theme` section in the documentation of each individual component.
|
||||
- Browse the source code directly in the GitHub repository at [`v3/src/theme`](https://github.com/nuxt/ui/tree/v3/src/theme).
|
||||
|
||||
::
|
||||
|
||||
### Config
|
||||
|
||||
::framework-only
|
||||
#nuxt
|
||||
::div
|
||||
|
||||
:::div
|
||||
You can override the theme of components globally inside your `app.config.ts` by using the exact same structure as the theme object.
|
||||
|
||||
Let's say you want to change the font weight of all your buttons, you can do it like this:
|
||||
@@ -781,14 +862,21 @@ export default defineAppConfig({
|
||||
})
|
||||
```
|
||||
|
||||
::
|
||||
:::
|
||||
|
||||
#vue
|
||||
::div
|
||||
|
||||
:::div
|
||||
You can override the theme of components globally inside your `vite.config.ts` by using the exact same structure as the theme object.
|
||||
|
||||
Let's say you want to change the font weight of all your buttons, you can do it like this:
|
||||
|
||||
::::module-only
|
||||
|
||||
#ui
|
||||
|
||||
:::::div
|
||||
|
||||
```ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
@@ -810,7 +898,38 @@ export default defineConfig({
|
||||
})
|
||||
```
|
||||
|
||||
::
|
||||
:::::
|
||||
|
||||
#ui-pro
|
||||
|
||||
:::::div
|
||||
|
||||
```ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import uiPro from '@nuxt/ui-pro/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
uiPro({
|
||||
ui: {
|
||||
button: {
|
||||
slots: {
|
||||
base: 'font-bold'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
:::::
|
||||
|
||||
::::
|
||||
|
||||
:::
|
||||
|
||||
::
|
||||
|
||||
|
||||
@@ -99,9 +99,8 @@ In your `nuxt.config.ts`, add an item in `icon.customCollections`:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxt/ui'
|
||||
],
|
||||
modules: ['@nuxt/ui'],
|
||||
css: ['~/assets/css/main.css'],
|
||||
icon: {
|
||||
customCollections: [{
|
||||
prefix: 'custom',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user