Compare commits

..

1 Commits

Author SHA1 Message Date
Romain Hamel
3a56de3be0 fix(form): provide typings for $el 2024-11-08 12:18:30 +01:00
183 changed files with 2869 additions and 8881 deletions

View File

@@ -5,8 +5,8 @@ body:
- type: markdown
attributes:
value: |
> [!IMPORTANT]
> 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.
> [!IMPORTANT]
> 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.
- type: markdown
attributes:
value: |
@@ -29,20 +29,11 @@ body:
- Build Modules: `-`
validations:
required: true
- type: dropdown
id: package
attributes:
label: Is this bug related to Nuxt or Vue?
options:
- Nuxt
- Vue
validations:
required: true
- type: input
id: version
attributes:
label: Version
placeholder: v3.0.0-alpha.x
placeholder: v3.0.0-alpha.5
validations:
required: true
- type: textarea

View File

@@ -12,7 +12,7 @@ body:
label: For what version of Nuxt UI are you suggesting this?
options:
- v2.x
- v3.0.0-alpha.x
- v3-alpha
validations:
required: true
- type: textarea

View File

@@ -12,7 +12,7 @@ body:
label: For what version of Nuxt UI are you asking this question?
options:
- v2.x
- v3.0.0-alpha.x
- v3-alpha
validations:
required: true
- type: textarea

View File

@@ -61,8 +61,5 @@ jobs:
- name: Build
run: pnpm run build
- name: Build vue fixture
run: pnpm run test:vue:build
- name: Publish
run: pnpx pkg-pr-new publish --compact --no-template --pnpm

View File

@@ -1,67 +1,5 @@
# Changelog
## [3.0.0-alpha.9](https://github.com/nuxt/ui/compare/v3.0.0-alpha.8...v3.0.0-alpha.9) (2024-11-19)
### Features
* **cli:** add locale command ([#2586](https://github.com/nuxt/ui/issues/2586)) ([824ba56](https://github.com/nuxt/ui/commit/824ba5629183bc4cd59321213558770db211f6e5))
* **css:** add `--ui-bg-muted` / `--ui-border-muted` variables ([7f6db45](https://github.com/nuxt/ui/commit/7f6db45f1e15ef39cda9b732cb601c552f29570a))
* **Form:** apply transformations ([#2550](https://github.com/nuxt/ui/issues/2550)) ([75c5e87](https://github.com/nuxt/ui/commit/75c5e87724e7abdf0a6751d7a1dbbafb947f373f))
* **FormField:** add `error-pattern` prop ([#2601](https://github.com/nuxt/ui/issues/2601)) ([143612e](https://github.com/nuxt/ui/commit/143612ec737d1c7571398601c3222f2eed37996e))
* **InputMenu/SelectMenu:** add `create-item` prop ([#2472](https://github.com/nuxt/ui/issues/2472)) ([f516d7b](https://github.com/nuxt/ui/commit/f516d7b36da51565f4ab05a4c9cfe5e5b4015124))
* **InputNumber:** implement component ([#2577](https://github.com/nuxt/ui/issues/2577)) ([bd2f077](https://github.com/nuxt/ui/commit/bd2f077fe8e645d5fce8b1eb5a6eb1068b3e8f7c))
* **Link:** allow partial query match for its active state ([#2664](https://github.com/nuxt/ui/issues/2664)) ([7329900](https://github.com/nuxt/ui/commit/7329900ae549430b88567a09cbb585d3cf0a6d32))
* **locale:** add Persian language ([#2682](https://github.com/nuxt/ui/issues/2682)) ([14fb21b](https://github.com/nuxt/ui/commit/14fb21be0034ffc0ba5d213734c00f12e0d6bea8))
* **locale:** add Polish language ([#2678](https://github.com/nuxt/ui/issues/2678)) ([2fc36c8](https://github.com/nuxt/ui/commit/2fc36c878c67967ec91e4f6999575bad45521d44))
* **locale:** add support for Arabic ([#2582](https://github.com/nuxt/ui/issues/2582)) ([602a667](https://github.com/nuxt/ui/commit/602a667343be22b72383ab3cf42f36ec9e135082))
* **locale:** add support for Czech translation ([#2593](https://github.com/nuxt/ui/issues/2593)) ([4889d30](https://github.com/nuxt/ui/commit/4889d30b448296de42e146dc5771738837c31f8c))
* **locale:** add support for Italian ([#2583](https://github.com/nuxt/ui/issues/2583)) ([4fbbb25](https://github.com/nuxt/ui/commit/4fbbb25f68b0b5ee76e50f2da776a74d54acc041))
* **locale:** provide `code` ([#2611](https://github.com/nuxt/ui/issues/2611)) ([8a8b1ee](https://github.com/nuxt/ui/commit/8a8b1ee2e1628bc5439ef109d3c68b69bf631f81))
* **locale:** provide `dir` on `defineLocale` ([#2620](https://github.com/nuxt/ui/issues/2620)) ([937585c](https://github.com/nuxt/ui/commit/937585cb3f8bc902d60a4b5904711598204aee2d))
* **locale:** translate chinese ([#2580](https://github.com/nuxt/ui/issues/2580)) ([febda5c](https://github.com/nuxt/ui/commit/febda5c2b67374d1358a66694543b77037d239c6))
* **locale:** translate Spanish ([#2644](https://github.com/nuxt/ui/issues/2644)) ([8ed434c](https://github.com/nuxt/ui/commit/8ed434c105b75ae02aa7493a235cebb64d518d09))
* **locale:** typing `dir` ([#2643](https://github.com/nuxt/ui/issues/2643)) ([e55c0e2](https://github.com/nuxt/ui/commit/e55c0e25947e7bcef931b26dafaad120f488a627))
* **module:** support i18n in components ([#2553](https://github.com/nuxt/ui/issues/2553)) ([2636240](https://github.com/nuxt/ui/commit/26362408b161108487b889ff001bec9166059c79))
* **NavigationMenu:** control items `open` & `defaultOpen` on vertical ([30218f1](https://github.com/nuxt/ui/commit/30218f1b5b0a56207fd4db224ffa0401ac194a04)), closes [#2608](https://github.com/nuxt/ui/issues/2608)
* **PinInput:** implement component ([#2570](https://github.com/nuxt/ui/issues/2570)) ([95aa6f6](https://github.com/nuxt/ui/commit/95aa6f68b316d02c28d1124d9a826bca55cde81f))
* **Popover:** add `prevent-close` prop ([ea97759](https://github.com/nuxt/ui/commit/ea97759c2c219bdf5c48b652b47d293ddf513a00)), closes [#2245](https://github.com/nuxt/ui/issues/2245)
* **SelectMenu:** use `UInput` in search to handle props like icon ([ff1e079](https://github.com/nuxt/ui/commit/ff1e0798d384d40ad82a95fe5faa16acb080efe3)), closes [#2021](https://github.com/nuxt/ui/issues/2021)
* **Table:** add `caption` prop ([446f9c1](https://github.com/nuxt/ui/commit/446f9c1085e96187afdc5c1d7ce3450f8df1a2e1))
### Bug Fixes
* **App:** missing `vue` imports ([ddb4690](https://github.com/nuxt/ui/commit/ddb46905e7e3480ab578bcd8a478f25dff60081a))
* **App:** remove `dir` prop ([#2630](https://github.com/nuxt/ui/issues/2630)) ([7cc26d0](https://github.com/nuxt/ui/commit/7cc26d098dff70923bcd9d414d675018951b1967))
* **Breadcrumb/Carousel/Pagination:** handle icons in RTL mode ([#2633](https://github.com/nuxt/ui/issues/2633)) ([e5119a9](https://github.com/nuxt/ui/commit/e5119a9ca4e217ef769904323c16bd8c0cbc02d9))
* **Breadcrumb:** render as `nav` ([756f791](https://github.com/nuxt/ui/commit/756f791a1a8dd3a4a88c212b4e4f775584decb55)), closes [#2649](https://github.com/nuxt/ui/issues/2649)
* **Button:** improve neutral solid variant hover ([8d85498](https://github.com/nuxt/ui/commit/8d85498ee197ec0b26cdd7c4b08f84fec45ddd8f))
* **Carousel:** use `dir` from locale ([#2647](https://github.com/nuxt/ui/issues/2647)) ([1fbbfe8](https://github.com/nuxt/ui/commit/1fbbfe8df06b3b8b294615ac328d582c5230aa8b))
* **ContextMenu/DropdownMenu:** relative imports with prefix ([47f58f5](https://github.com/nuxt/ui/commit/47f58f52ef2d03176a184a3ca2154f5cea655edb))
* **css:** `--font-family-sans` renamed to `--font-sans` ([#2680](https://github.com/nuxt/ui/issues/2680)) ([b2fa657](https://github.com/nuxt/ui/commit/b2fa65734bb59186520c985f7c73fc34a0cb8b37))
* **css:** remove useless spacing override ([8d00265](https://github.com/nuxt/ui/commit/8d0026558a21efbbca08e9939844f7479a0d6cce))
* **FormField:** missing conditions to apply container classes ([#2631](https://github.com/nuxt/ui/issues/2631)) ([9241ba1](https://github.com/nuxt/ui/commit/9241ba1230b0fde41595634362d83c92c66b7699))
* **Form:** match `error-pattern` on input validation ([#2606](https://github.com/nuxt/ui/issues/2606)) ([3584a33](https://github.com/nuxt/ui/commit/3584a3328b8588f024557c9908242bc374853419))
* **InputMenu/SelectMenu:** init `filter` with `labelKey` ([18931ac](https://github.com/nuxt/ui/commit/18931acdb316bc72a3e5ed6d20985688ad5c8d99))
* **InputMenu/SelectMenu:** look in `items` only with `value-attribute` ([0ceafe1](https://github.com/nuxt/ui/commit/0ceafe1d54000f3eb49562b00c188d82fa23c4ee)), closes [#2464](https://github.com/nuxt/ui/issues/2464)
* **InputMenu/SelectMenu:** multiple not working with generic boolean casting ([503f701](https://github.com/nuxt/ui/commit/503f701c7ecdfe27e9057e5ddebfc7e03889d61b)), closes [#2541](https://github.com/nuxt/ui/issues/2541)
* **InputMenu/SelectMenu:** use `isEqual` from `ohash` ([f943f88](https://github.com/nuxt/ui/commit/f943f88fcc9f4678d8f7bd224799e289a0c57dd8))
* **Link:** missing relative import ([#2588](https://github.com/nuxt/ui/issues/2588)) ([95a0bbc](https://github.com/nuxt/ui/commit/95a0bbc581a40677f620eed3170f9a423976214b))
* **locale:** Improve German translation ([#2676](https://github.com/nuxt/ui/issues/2676)) ([992be91](https://github.com/nuxt/ui/commit/992be91823fe1877254ccd092c71c77dd3ff42f7))
* **locale:** it translation ([#2623](https://github.com/nuxt/ui/issues/2623)) ([73e25ed](https://github.com/nuxt/ui/commit/73e25ed23562f755ea4c66e6c5fb06dd18caac1e))
* **locale:** Italian translation ([#2584](https://github.com/nuxt/ui/issues/2584)) ([d167c9b](https://github.com/nuxt/ui/commit/d167c9b807a82fdf0fd280ce8417a66f86d7ed72))
* **Modal/Slideover:** prevent `esc` with `prevent-close` prop ([9e2cc5b](https://github.com/nuxt/ui/commit/9e2cc5b12567472044726924a3896b4b0e7993a1)), closes [#2501](https://github.com/nuxt/ui/issues/2501)
* **module:** remove `fast-deep-equal` in favor of custom `isEqual` ([37a3597](https://github.com/nuxt/ui/commit/37a359701f4b2ce4a9b0727b64c0e3eea6be00b4))
* **module:** skip devtools renderer page injection if router integration is disabled ([#2571](https://github.com/nuxt/ui/issues/2571)) ([afe4003](https://github.com/nuxt/ui/commit/afe40033b088d8aedb73fa8387a0284ef78444e4))
* **PinInput:** missing `useFormField` import ([601f4b2](https://github.com/nuxt/ui/commit/601f4b2cd2027817b935e02a0b4584dc3dce655f))
* **Textarea:** `autoresize` does not work when initializing `modelValue` ([#2681](https://github.com/nuxt/ui/issues/2681)) ([d3a079a](https://github.com/nuxt/ui/commit/d3a079a644db3dfe2f4e9567973550d74b3ba905))
* **Toaster:** teleport to `body` ([b0be26d](https://github.com/nuxt/ui/commit/b0be26d67feab467ac5862edd82e52df03a5091c)), closes [#2404](https://github.com/nuxt/ui/issues/2404)
* **Toast:** unreachable behind overlays ([#2650](https://github.com/nuxt/ui/issues/2650)) ([0daac5b](https://github.com/nuxt/ui/commit/0daac5bafb756c3a2dfaf2bf166c30c0eb476e08))
* **useLocale:** missing import in various components ([#2603](https://github.com/nuxt/ui/issues/2603)) ([df7a61a](https://github.com/nuxt/ui/commit/df7a61a97a14b3d7943baee6a74686134dfdb10b))
### Reverts
* Revert "docs(ComponentCode/ComponentExample): use relative imports" ([5deadc7](https://github.com/nuxt/ui/commit/5deadc709640bbfd3ec14c1c9363deb55e765d6a))
## [3.0.0-alpha.8](https://github.com/nuxt/ui/compare/v3.0.0-alpha.7...v3.0.0-alpha.8) (2024-11-07)
### ⚠ BREAKING CHANGES

View File

@@ -1,6 +1,6 @@
[![nuxt-ui.png](https://repository-images.githubusercontent.com/428329515/43fec891-9030-4601-8233-5d45ba5c6013)](https://ui.nuxt.com)
# Nuxt UI
# Nuxt UI v3
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
@@ -9,15 +9,10 @@
We're thrilled to introduce Nuxt UI v3, a significant upgrade to our UI library that delivers extensive improvements and robust new capabilities. This major update harnesses the combined strengths of [Radix Vue](https://www.radix-vue.com/), [Tailwind CSS v4](https://tailwindcss.com/blog/tailwindcss-v4-alpha), and [Tailwind Variants](https://www.tailwind-variants.org/) to offer developers an unparalleled set of tools for creating sophisticated, accessible, and highly performant user interfaces.
> [!NOTE]
> You are on the `v3` development branch, check out the [dev branch](https://github.com/nuxt/ui) for Nuxt UI v2.
## Documentation
Visit https://ui3.nuxt.dev to explore the documentation.
## Installation
1. Install the Nuxt UI v3 alpha package:
```bash [pnpm]
pnpm add @nuxt/ui@next
```
@@ -34,9 +29,10 @@ npm install @nuxt/ui@next
bun add @nuxt/ui@next
```
### Nuxt
> [!WARNING]
> Make sure you have `typescript` installed in your dev dependencies.
1. Add the Nuxt UI module in your `nuxt.config.ts`:
2. Register the Nuxt UI module in your `nuxt.config.ts`:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
@@ -44,54 +40,18 @@ export default defineNuxtConfig({
})
```
2. Import Tailwind CSS and Nuxt UI in your CSS:
3. Import Tailwind CSS and Nuxt UI in your `app.vue` or [CSS](https://nuxt.com/docs/getting-started/styling#the-css-property):
```css [assets/css/main.css]
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
</style>
```
Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/installation/nuxt).
## Documentation
### Vue
1. Add the Nuxt UI Vite plugin in your `vite.config.ts`:
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui()
]
})
```
2. Use the Nuxt UI Vue plugin in your `main.ts`:
```ts [main.ts]
import { createApp } from 'vue'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
app.use(ui)
app.mount('#app')
```
3. Import Tailwind CSS and Nuxt UI in your CSS:
```css [assets/main.css]
@import "tailwindcss";
@import "@nuxt/ui";
```
Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/installation/vue).
Visit https://ui3.nuxt.dev to explore the documentation.
## Credits

View File

@@ -3,13 +3,13 @@ import { resolve } from 'pathe'
import { defineCommand } from 'citty'
import { consola } from 'consola'
import { splitByCase, upperFirst, camelCase, kebabCase } from 'scule'
import { appendFile, sortFile } from '../../utils.mjs'
import templates from '../../templates.mjs'
import { appendFile, sortFile } from '../utils.mjs'
import templates from '../templates.mjs'
export default defineCommand({
meta: {
name: 'component',
description: 'Make a new component.'
name: 'init',
description: 'Init a new component.'
},
args: {
name: {

View File

@@ -1,14 +0,0 @@
import { defineCommand } from 'citty'
import component from './component.mjs'
import locale from './locale.mjs'
export default defineCommand({
meta: {
name: 'make',
description: 'Commands to create new Nuxt UI entities.'
},
subCommands: {
component,
locale
}
})

View File

@@ -1,64 +0,0 @@
import { existsSync, promises as fsp } from 'node:fs'
import { resolve } from 'pathe'
import { consola } from 'consola'
import { appendFile, sortFile, normalizeLocale } from '../../utils.mjs'
import { defineCommand } from 'citty'
export default defineCommand({
meta: {
name: 'locale',
description: 'Make a new locale.'
},
args: {
code: {
description: 'Locale code to create. For example: en.',
required: true
},
name: {
description: 'Locale name to create. For example: English.',
required: true
},
dir: {
description: 'Locale direction. For example: rtl.',
default: 'ltr'
}
},
async setup({ args }) {
const path = resolve('.')
const localePath = resolve(path, `src/runtime/locale`)
const originLocaleFilePath = resolve(localePath, 'en.ts')
const newLocaleFilePath = resolve(localePath, `${args.code}.ts`)
// Validate locale code
if (existsSync(newLocaleFilePath)) {
consola.error(`🚨 ${args.code} already exists!`)
process.exit(1)
}
if (!['ltr', 'rtl'].includes(args.dir)) {
consola.error(`🚨 Direction ${args.dir} not supported!`)
process.exit(1)
}
if (!args.code.match(/^[a-z]{2}(?:_[a-z]{2,4})?$/)) {
consola.error(`🚨 ${args.code} is not a valid locale code!\nExample: en or en_us`)
process.exit(1)
}
// Create new locale export
const localeExportFile = resolve(localePath, `index.ts`)
await appendFile(localeExportFile, `export { default as ${args.code} } from './${args.code}'`)
await sortFile(localeExportFile)
// Create new locale file
await fsp.copyFile(originLocaleFilePath, newLocaleFilePath)
const localeFile = await fsp.readFile(newLocaleFilePath, 'utf-8')
const rewrittenLocaleFile = localeFile
.replace(/name: '(.*)',/, `name: '${args.name}',`)
.replace(/code: '(.*)',/, `code: '${normalizeLocale(args.code)}',${(args.dir && args.dir !== 'ltr') ? `\n dir: '${args.dir}',` : ''}`)
await fsp.writeFile(newLocaleFilePath, rewrittenLocaleFile)
consola.success(`🪄 Generated ${newLocaleFilePath}`)
}
})

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { defineCommand, runMain } from 'citty'
import make from './commands/make/index.mjs'
import init from './commands/init.mjs'
const main = defineCommand({
meta: {
@@ -8,7 +8,7 @@ const main = defineCommand({
description: 'Nuxt UI CLI'
},
subCommands: {
make
init
}
})

View File

@@ -163,54 +163,9 @@ describe('${upperName}', () => {
}
}
const doc = ({ name, pro }) => {
const kebabName = kebabCase(name)
const upperName = splitByCase(name).map(p => upperFirst(p)).join('')
return {
filename: `docs/content/${pro ? 'pro' : '3.components'}/${kebabName}.md`,
contents: `---
description:
links: ${pro
? ''
: `
- label: ${upperName}
icon: i-custom-radix-vue
to: https://www.radix-vue.com/components/${kebabName}.html`}
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/${pro ? 'ui-pro' : 'ui'}/tree/v3/src/runtime/components/${upperName}.vue
---
## Usage
## Examples
## API
### Props
:component-props
### Slots
:component-slots
### Emits
:component-emits
## Theme
:component-theme
`
}
}
export default {
playground,
component,
theme,
test,
doc
test
}

View File

@@ -15,17 +15,3 @@ export async function appendFile(path, contents) {
await fsp.writeFile(path, file.trim() + '\n' + contents + '\n')
}
}
export function normalizeLocale(locale) {
if (!locale) {
return ''
}
if (locale.includes('_')) {
return locale.split('_')
.map((part, index) => index === 0 ? part.toLowerCase() : part.toUpperCase())
.join('-')
}
return locale.toLowerCase()
}

View File

@@ -168,7 +168,7 @@ const isDark = computed({
@import '@nuxt/ui';
@theme {
--font-sans: 'DM Sans', sans-serif;
--font-family-sans: 'DM Sans', sans-serif;
--color-primary-50: var(--ui-color-primary-50);
--color-primary-100: var(--ui-color-primary-100);

View File

@@ -18,7 +18,7 @@ onMounted(() => {
</script>
<template>
<div class="border rounded-[var(--ui-radius)] border-[var(--ui-border)]">
<div class="border rounded border-[var(--ui-border)]">
<div
ref="wrapper"
:class="['overflow-hidden', collapsed && overflow ? 'max-h-48' : 'max-h-none']"

View File

@@ -126,7 +126,7 @@ const previewUrl = computed(() => {
</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-[var(--ui-border)] bg-neutral-50 dark:bg-neutral-800" v-html="highlightedCode" />
<pre class="p-4 min-h-40 max-h-72 text-sm overflow-y-auto rounded-lg border border-[var(--ui-border)] bg-neutral-50 dark:bg-neutral-800" v-html="highlightedCode" />
<UButton
color="neutral"
variant="link"

View File

@@ -17,14 +17,14 @@ watchEffect(() => {
})
const description = computed(() => {
return props.meta.description?.replace(/`([^`]+)`/g, '<code class="font-medium bg-[var(--ui-bg-elevated)] px-1 py-0.5 rounded-[var(--ui-radius)]">$1</code>')
return props.meta.description?.replace(/`([^`]+)`/g, '<code class="font-medium bg-[var(--ui-bg-elevated)] px-1 py-0.5 rounded">$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-[var(--ui-border-accented)] border-dashed rounded-[var(--ui-radius)] bg-[var(--ui-bg-elevated)]">
<p v-if="meta?.name" class="font-mono font-bold px-1.5 py-0.5 border border-[var(--ui-border-accented)] border-dashed rounded bg-[var(--ui-bg-elevated)]">
{{ meta?.name }}
</p>
</template>

View File

@@ -8,9 +8,7 @@ const appConfig = useAppConfig()
const colorMode = useColorMode()
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('content'))
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('content'), {
server: false
})
const { data: files } = await useAsyncData('files', () => queryCollectionSearchSections('content', { ignoredTags: ['style'] }))
const searchTerm = ref('')
@@ -81,11 +79,6 @@ const updatedNavigation = computed(() => navigation.value?.map(item => ({
title: 'Installation',
active: route.path.startsWith('/getting-started/installation'),
children: []
}),
...(child.path === '/getting-started/i18n' && {
title: 'I18n',
active: route.path.startsWith('/getting-started/i18n'),
children: []
})
})) || []
})))
@@ -124,9 +117,7 @@ provide('navigation', updatedNavigation)
@source "../content/**/*.md";
@theme {
--container-8xl: 90rem;
--font-sans: 'Public Sans', sans-serif;
--font-family-sans: 'Public Sans', sans-serif;
--color-green-50: #EFFDF5;
--color-green-100: #D9FBE8;
@@ -142,6 +133,6 @@ provide('navigation', updatedNavigation)
}
:root {
--ui-container: var(--container-8xl);
--ui-container-width: 90rem;
}
</style>

View File

@@ -164,7 +164,7 @@ const code = computed(() => {
continue
}
code += ` ${typeof value === 'number' ? ':' : ''}${name}="${value}"`
code += ` ${prop?.type.includes('number') ? ':' : ''}${name}="${value}"`
}
}
@@ -220,7 +220,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-[var(--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 items-center gap-2.5 border border-[var(--ui-color-neutral-200)] dark:border-[var(--ui-color-neutral-700)] 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"
@@ -269,7 +269,7 @@ const { data: ast } = await useAsyncData(`component-code-${name}-${hash({ props:
</template>
</div>
<div v-if="component" class="flex justify-center border border-b-0 border-[var(--ui-border-muted)] relative p-4 z-[1]" :class="[!options.length && 'rounded-t-[calc(var(--ui-radius)*1.5)]', props.class]">
<div v-if="component" class="flex justify-center border border-b-0 border-[var(--ui-color-neutral-200)] dark:border-[var(--ui-color-neutral-700)] relative p-4 z-[1]" :class="[!options.length && 'rounded-t-[calc(var(--ui-radius)*1.5)]', props.class]">
<component :is="component" v-bind="{ ...componentProps, ...componentEvents }">
<template v-for="slot in Object.keys(slots || {})" :key="slot" #[slot]>
<MDCSlot :name="slot" unwrap="p">

View File

@@ -117,8 +117,8 @@ const optionsValues = ref(props.options?.reduce((acc, option) => {
<template>
<div class="my-5">
<template v-if="preview">
<div class="border border-[var(--ui-border-muted)] relative z-[1]" :class="[{ 'border-b-0 rounded-t-[calc(var(--ui-radius)*1.5)]': props.source, 'rounded-[calc(var(--ui-radius)*1.5)]': !props.source }]">
<div v-if="props.options?.length || !!slots.options" class="flex gap-4 p-4 border-b border-[var(--ui-border-muted)]">
<div class="border border-[var(--ui-color-neutral-200)] dark:border-[var(--ui-color-neutral-700)] relative z-[1]" :class="[{ 'border-b-0 rounded-t-[calc(var(--ui-radius)*1.5)]': props.source, 'rounded-[calc(var(--ui-radius)*1.5)]': !props.source }]">
<div v-if="props.options?.length || !!slots.options" class="flex gap-4 p-4 border-b border-[var(--ui-color-neutral-200)] dark:border-[var(--ui-color-neutral-700)]">
<slot name="options" />
<UFormField

View File

@@ -1,64 +0,0 @@
<script setup lang="ts">
import * as locales from '@nuxt/ui/locale'
import type { Locale } from '@nuxt/ui'
type LocaleKey = keyof typeof locales
const props = withDefaults(defineProps<{
default?: string
}>(), {
default: 'en'
})
const getLocaleKeys = Object.keys(locales) as LocaleKey[]
const localesList = getLocaleKeys.map<[LocaleKey, Locale]>(locale => [locale, locales[locale]])
</script>
<!-- eslint-disable vue/singleline-html-element-content-newline -->
<template>
<div>
<ProseP>
By default, the <ProseCode>{{ props.default }}</ProseCode> locale is used.
</ProseP>
<ProseTable>
<ProseThead>
<ProseTr>
<ProseTh>
Language
</ProseTh>
<ProseTh>
Code
</ProseTh>
<ProseTh>
Direction
</ProseTh>
</ProseTr>
</ProseThead>
<ProseTbody>
<ProseTr v-for="[key, locale] in localesList" :key="key">
<ProseTd>
{{ locale.name }}
</ProseTd>
<ProseTd>
<ProseCode>
{{ locale.code }}
</ProseCode>
</ProseTd>
<ProseTd>
<ProseCode>
{{ locale.dir }}
</ProseCode>
</ProseTd>
</ProseTr>
</ProseTbody>
</ProseTable>
<Note to="https://github.com/nuxt/ui/tree/v3/src/runtime/locale" target="_blank">
If you need additional languages, you can contribute by creating a PR to add a new locale in <ProseCode>src/runtime/locale/</ProseCode>.
</Note>
<Tip>
You can use the <ProseCode>nuxt-ui</ProseCode> CLI to create a new locale:
<ProsePre language="bash">nuxt-ui make locale --code "en" --name "English"</ProsePre>
</Tip>
</div>
</template>

View File

@@ -4,7 +4,6 @@ import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
input: z.string().min(10),
inputNumber: z.number().min(10),
inputMenu: z.any().refine(option => option?.value === 'option-2', {
message: 'Select Option 2'
}),
@@ -30,11 +29,10 @@ const schema = z.object({
radioGroup: z.string().refine(value => value === 'option-2', {
message: 'Select Option 2'
}),
slider: z.number().max(20, { message: 'Must be less than 20' }),
pin: z.string().regex(/^\d$/).array().length(5)
slider: z.number().max(20, { message: 'Must be less than 20' })
})
type Schema = z.input<typeof schema>
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({})
@@ -54,10 +52,10 @@ async function onSubmit(event: FormSubmitEvent<any>) {
</script>
<template>
<UForm ref="form" :state="state" :schema="schema" class="w-full" @submit="onSubmit">
<UForm ref="form" :state="state" :schema="schema" @submit="onSubmit">
<div class="grid grid-cols-3 gap-4">
<UFormField label="Input" name="input">
<UInput v-model="state.input" placeholder="john@lennon.com" class="w-full" />
<UInput v-model="state.input" placeholder="john@lennon.com" class="w-40" />
</UFormField>
<div class="flex flex-col gap-4">
@@ -75,48 +73,42 @@ async function onSubmit(event: FormSubmitEvent<any>) {
</UFormField>
<UFormField name="select" label="Select">
<USelect v-model="state.select" :items="items" class="w-full" />
<USelect v-model="state.select" :items="items" />
</UFormField>
<UFormField name="selectMenu" label="Select Menu">
<USelectMenu v-model="state.selectMenu" :items="items" class="w-full" />
<USelectMenu v-model="state.selectMenu" :items="items" />
</UFormField>
<UFormField name="selectMenuMultiple" label="Select Menu (Multiple)">
<USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" class="w-full" />
<USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" />
</UFormField>
<UFormField name="inputMenu" label="Input Menu">
<UInputMenu v-model="state.inputMenu" :items="items" class="w-full" />
<UInputMenu v-model="state.inputMenu" :items="items" />
</UFormField>
<UFormField name="inputMenuMultiple" label="Input Menu (Multiple)">
<UInputMenu v-model="state.inputMenuMultiple" multiple :items="items" class="w-full" />
<UInputMenu v-model="state.inputMenuMultiple" multiple :items="items" />
</UFormField>
<UFormField name="inputNumber" label="Input Number">
<UInputNumber v-model="state.inputNumber" class="w-full" />
</UFormField>
<span />
<UFormField label="Textarea" name="textarea">
<UTextarea v-model="state.textarea" class="w-full" />
<UTextarea v-model="state.textarea" />
</UFormField>
<UFormField name="radioGroup">
<URadioGroup v-model="state.radioGroup" legend="Radio group" :items="items" />
</UFormField>
<UFormField name="pin" label="Pin Input" :error-pattern="/(pin)\..*/">
<UPinInput v-model="state.pin" />
</UFormField>
</div>
<div class="flex gap-2 mt-8">
<UButton type="submit">
<UButton color="neutral" type="submit">
Submit
</UButton>
<UButton variant="outline" @click="form?.clear()">
<UButton color="neutral" variant="outline" @click="form?.clear()">
Clear
</UButton>
</div>

View File

@@ -1,39 +0,0 @@
<script setup lang="ts">
const { data: countries, status, execute } = await useLazyFetch<{
name: string
code: string
emoji: string
}[]>('/api/countries.json', {
immediate: false
})
function onOpen() {
if (!countries.value?.length) {
execute()
}
}
</script>
<template>
<UInputMenu
:items="countries || []"
:loading="status === 'pending'"
label-key="name"
:search-input="{ icon: 'i-lucide-search' }"
placeholder="Select country"
class="w-48"
@update:open="onOpen"
>
<template #leading="{ modelValue, ui }">
<span v-if="modelValue" class="size-5 text-center">
{{ modelValue?.emoji }}
</span>
<UIcon v-else name="i-lucide-earth" :class="ui.leadingIcon()" />
</template>
<template #item-leading="{ item }">
<span class="size-5 text-center">
{{ item.emoji }}
</span>
</template>
</UInputMenu>
</template>

View File

@@ -16,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
<UInputMenu
:items="users || []"
:loading="status === 'pending'"
:filter="['label', 'email']"
:filter="['name', 'email']"
icon="i-lucide-user"
placeholder="Select user"
class="w-80"

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
const value = ref(1500)
</script>
<template>
<UInputNumber
v-model="value"
:format-options="{
style: 'currency',
currency: 'EUR',
currencyDisplay: 'code',
currencySign: 'accounting'
}"
/>
</template>

View File

@@ -1,13 +0,0 @@
<script setup lang="ts">
const value = ref(5)
</script>
<template>
<UInputNumber
v-model="value"
:format-options="{
signDisplay: 'exceptZero',
minimumFractionDigits: 1
}"
/>
</template>

View File

@@ -1,9 +0,0 @@
<script setup lang="ts">
const retries = ref(0)
</script>
<template>
<UFormField label="Retries" help="Specify number of attempts" required>
<UInputNumber v-model="retries" placeholder="Enter retries" />
</UFormField>
</template>

View File

@@ -1,13 +0,0 @@
<script setup lang="ts">
const value = ref(0.05)
</script>
<template>
<UInputNumber
v-model="value"
:step="0.01"
:format-options="{
style: 'percent'
}"
/>
</template>

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
const value = ref(5)
</script>
<template>
<UInputNumber v-model="value">
<template #decrement>
<UButton size="xs" icon="i-lucide-minus" />
</template>
<template #increment>
<UButton size="xs" icon="i-lucide-plus" />
</template>
</UInputNumber>
</template>

View File

@@ -6,7 +6,7 @@ const value = ref('Click to clear')
<UInput
v-model="value"
placeholder="Type something..."
:ui="{ trailing: 'pe-1' }"
:ui="{ trailing: 'pr-0.5' }"
>
<template v-if="value?.length" #trailing>
<UButton

View File

@@ -1,33 +0,0 @@
<script setup lang="ts">
const value = ref('npx nuxi module add ui')
const copied = ref(false)
function copy() {
navigator.clipboard.writeText(value.value)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
}
</script>
<template>
<UInput
v-model="value"
:ui="{ trailing: 'pr-0.5' }"
>
<template v-if="value?.length" #trailing>
<UTooltip text="Copy to clipboard" :content="{ side: 'right' }">
<UButton
:color="copied ? 'success' : 'neutral'"
variant="link"
size="sm"
:icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
aria-label="Copy to clipboard"
@click="copy"
/>
</UTooltip>
</template>
</UInput>
</template>

View File

@@ -1,21 +0,0 @@
<script setup lang="ts">
const input = useTemplateRef('input')
defineShortcuts({
'/': () => {
input.value?.inputRef?.focus()
}
})
</script>
<template>
<UInput
ref="input"
icon="i-lucide-search"
placeholder="Search..."
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
</template>

View File

@@ -40,7 +40,7 @@ const text = computed(() => {
placeholder="Password"
:color="color"
:type="show ? 'text' : 'password'"
:ui="{ trailing: 'pe-1' }"
:ui="{ trailing: 'pr-0.5' }"
:aria-invalid="score < 4"
aria-describedby="password-strength"
class="w-full"

View File

@@ -8,7 +8,7 @@ const password = ref('password')
v-model="password"
placeholder="Password"
:type="show ? 'text' : 'password'"
:ui="{ trailing: 'pe-1' }"
:ui="{ trailing: 'pr-0.5' }"
>
<template #trailing>
<UButton

View File

@@ -1,40 +0,0 @@
<script setup lang="ts">
const { data: countries, status, execute } = await useLazyFetch<{
name: string
code: string
emoji: string
}[]>('/api/countries.json', {
immediate: false,
default: () => []
})
function onOpen() {
if (!countries.value?.length) {
execute()
}
}
</script>
<template>
<USelectMenu
:items="countries"
:loading="status === 'pending'"
label-key="name"
:search-input="{ icon: 'i-lucide-search' }"
placeholder="Select country"
class="w-48"
@update:open="onOpen"
>
<template #leading="{ modelValue, ui }">
<span v-if="modelValue" class="size-5 text-center">
{{ modelValue?.emoji }}
</span>
<UIcon v-else name="i-lucide-earth" :class="ui.leadingIcon()" />
</template>
<template #item-leading="{ item }">
<span class="size-5 text-center">
{{ item.emoji }}
</span>
</template>
</USelectMenu>
</template>

View File

@@ -16,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
<USelectMenu
:items="users || []"
:loading="status === 'pending'"
:filter="['label', 'email']"
:filter="['name', 'email']"
icon="i-lucide-user"
placeholder="Select user"
class="w-80"

View File

@@ -7,9 +7,9 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.duration"
size="sm"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
:ui="{
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
container: 'mt-0'
}"
@@ -18,7 +18,8 @@ const appConfig = useAppConfig()
v-model="appConfig.toaster.duration"
color="neutral"
variant="soft"
:ui="{ base: 'rounded-[var(--ui-radius)] rounded-l-none min-w-12' }"
class="rounded rounded-l-none min-w-12"
:search-input="false"
/>
</UFormField>
</div>

View File

@@ -7,9 +7,9 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.expand"
size="sm"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
:ui="{
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
container: 'mt-0'
}"
@@ -19,7 +19,7 @@ const appConfig = useAppConfig()
:items="[true, false]"
color="neutral"
variant="soft"
class="rounded-[var(--ui-radius)] rounded-l-none min-w-12"
class="rounded rounded-l-none min-w-12"
:search-input="false"
/>
</UFormField>

View File

@@ -10,9 +10,9 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.position"
size="sm"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
:ui="{
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
container: 'mt-0'
}"
@@ -22,7 +22,7 @@ const appConfig = useAppConfig()
:items="positions"
color="neutral"
variant="soft"
class="rounded-[var(--ui-radius)] rounded-l-none min-w-12"
class="rounded rounded-l-none min-w-12"
:search-input="false"
/>
</UFormField>

View File

@@ -14,9 +14,7 @@ select:
## Setup
::steps{level="4"}
#### Install the Nuxt UI v3 alpha package
1. Install the Nuxt UI v3 alpha package:
::code-group{sync="pm"}
@@ -39,10 +37,10 @@ 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@next` directly in your project's root directory.
::
#### Add the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}
2. Register the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
@@ -50,24 +48,15 @@ export default defineNuxtConfig({
})
```
#### Import Tailwind CSS and Nuxt UI in your CSS
3. Import Tailwind CSS and Nuxt UI in your `app.vue`{lang="ts-type"} or [CSS](https://nuxt.com/docs/getting-started/styling#the-css-property):
```css [assets/css/main.css]
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
</style>
```
::note
Use the `css` property in your `nuxt.config.ts` to import your CSS file.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css']
})
```
::
::tip
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:
```json
@@ -81,6 +70,8 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
::
::warning
IntelliSense works better when importing `tailwindcss` in a proper `.css` file which will be automatically detected.
::
## Options

View File

@@ -14,9 +14,7 @@ select:
## Setup
::steps{level="4"}
#### Install the Nuxt UI v3 alpha package
1. Install the Nuxt UI v3 alpha package:
::code-group{sync="pm"}
@@ -39,12 +37,12 @@ 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@next`, `vue-router` and `@unhead/vue` directly in your project's root directory.
::
#### Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}
2. Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}:
```ts [vite.config.ts]{3,8}
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
@@ -53,7 +51,7 @@ export default defineConfig({
plugins: [
vue(),
ui()
]
],
})
```
@@ -73,45 +71,28 @@ components.d.ts
```
::
#### Use the Nuxt UI Vue plugin in your `main.ts`
3. Register the Nuxt UI Vue plugin in your app:
```ts [main.ts]{2,7}
```ts [main.ts]
import { createApp } from 'vue'
import ui from '@nuxt/ui/vue-plugin'
import nuxtUI from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
app.use(ui)
// ...
app.use(nuxtUI)
app.mount('#app')
```
#### Import Tailwind CSS and Nuxt UI in your CSS
4. Import Tailwind CSS and Nuxt UI in your `App.vue`{lang="ts-type"} or CSS:
```css [assets/main.css]
```vue [App.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
</style>
```
::note
Import the CSS file in your `main.ts`.
```ts [main.ts]{1}
import './assets/main.css'
import { createApp } from 'vue'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
app.use(ui)
app.mount('#app')
```
::
::tip
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:
```json
@@ -125,6 +106,8 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
::
::warning
IntelliSense works better when importing `tailwindcss` in a proper `.css` file which will be automatically detected.
::
## Options

View File

@@ -11,12 +11,13 @@ Nuxt UI v3 uses Tailwind CSS v4 alpha which doesn't have a documentation yet, le
Tailwind CSS v4 takes a CSS-first configuration approach, you now customize your theme with CSS variables inside a `@theme` directive:
```css [main.css]
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
--font-sans: 'Public Sans', sans-serif;
--font-family-sans: 'Public Sans', sans-serif;
--breakpoint-3xl: 1920px;
@@ -32,6 +33,7 @@ Tailwind CSS v4 takes a CSS-first configuration approach, you now customize your
--color-green-900: #0A5331;
--color-green-950: #052E16;
}
</style>
```
The `@theme` directive tells Tailwind to make new utilities and variants available based on these variables. It's the equivalent of the `theme.extend` key in Tailwind CSS v3 `tailwind.config.ts` file.
@@ -46,11 +48,13 @@ You can use the `@source` directive to add explicit content glob patterns if you
This can be useful when writing Tailwind classes in markdown files with [`@nuxt/content`](https://github.com/nuxt/content):
```css [main.css]
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
@source "../content/**/*.md";
</style>
```
::note{to="https://github.com/tailwindlabs/tailwindcss/pull/14078"}
@@ -61,11 +65,13 @@ You can learn more about the `@source` directive in this pull request.
You can use the `@plugin` directive to import Tailwind CSS plugins.
```css [main.css]
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
@plugin "@tailwindcss/typography";
</style>
```
::note{to="https://github.com/tailwindlabs/tailwindcss/pull/14264"}
@@ -148,7 +154,8 @@ These color aliases are not automatically defined as Tailwind CSS colors, so cla
However, you can generate these classes using Tailwind's `@theme` directive, allowing you to use custom color utility classes while maintaining dynamic color aliases:
```css [main.css]
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
@@ -165,6 +172,7 @@ However, you can generate these classes using Tailwind's `@theme` directive, all
--color-primary-900: var(--ui-color-primary-900);
--color-primary-950: var(--ui-color-primary-950);
}
</style>
```
::
@@ -209,7 +217,8 @@ You can use these variables in classes like `text-[var(--ui-primary)]`, it will
::tip
You can change which shade is used for each color on light and dark mode:
```css [main.css]
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
@@ -220,6 +229,7 @@ You can change which shade is used for each color on light and dark mode:
.dark {
--ui-primary: var(--ui-color-primary-200);
}
</style>
```
::
@@ -314,7 +324,8 @@ body {
::tip
You can customize these CSS variables to tailor the appearance of your application:
```css [main.css]
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
@@ -327,6 +338,7 @@ You can customize these CSS variables to tailor the appearance of your applicati
--ui-bg: var(--ui-color-neutral-950);
--ui-border: var(--ui-color-neutral-900);
}
</style>
```
::
@@ -347,40 +359,15 @@ Try the :prose-icon{name="i-lucide-swatch-book" class="text-[var(--ui-primary)]"
::tip
You can customize the default radius value using the default Tailwind CSS variables or a value of your choice:
```css [main.css]
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
:root {
--ui-radius: var(--radius-sm);
}
```
::
#### Container
Nuxt UI uses a global `--ui-container` CSS variable to define the width of the container:
```css
:root {
--ui-container: var(--container-7xl);
}
```
::tip
You can customize the default container width using the default Tailwind CSS variables or a value of your choice:
```css [main.css]
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
--container-8xl: 90rem;
}
:root {
--ui-container: var(--container-8xl);
}
</style>
```
::
@@ -397,7 +384,7 @@ Components in Nuxt UI can have multiple `slots`, each representing a distinct HT
```ts [src/theme/card.ts]
export default {
slots: {
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow-sm',
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow',
header: 'p-4 sm:px-6',
body: 'p-4 sm:p-6',
footer: 'p-4 sm:px-6'
@@ -431,7 +418,7 @@ Some components don't have slots, they are just composed of a single root elemen
```ts [src/theme/container.ts]
export default {
base: 'max-w-[var(--ui-container)] mx-auto px-4 sm:px-6 lg:px-8'
base: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
}
```

View File

@@ -12,13 +12,15 @@ links:
Nuxt UI automatically registers the [`@nuxt/fonts`](https://github.com/nuxt/fonts) module for you, so there's no additional setup required. To use a font in your Nuxt UI application, you can simply declare it in your CSS:
```css [main.css]
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
--font-sans: 'Public Sans', sans-serif;
--font-family-sans: 'Public Sans', sans-serif;
}
</style>
```
That's it! Nuxt Fonts will detect this and you should immediately see the web font loaded in your browser.

View File

@@ -1 +0,0 @@
badge: New

View File

@@ -1,167 +0,0 @@
---
navigation.title: Nuxt
title: Internationalization (i18n) in a Nuxt app
description: 'Learn how to internationalize your Nuxt app with multi-directional support (LTR/RTL).'
select:
items:
- label: Nuxt
icon: i-logos-nuxt-icon
to: /getting-started/i18n/nuxt
- label: Vue
icon: i-logos-vue
to: /getting-started/i18n/vue
---
## Usage
::note{to="/components/app"}
Nuxt UI provides an [App](/components/app) component that wraps your app to provide global configurations.
::
### Locale
Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:
```vue [app.vue]
<script setup lang="ts">
import { fr } from '@nuxt/ui/locale'
</script>
<template>
<UApp :locale="fr">
<NuxtPage />
</UApp>
</template>
```
### Custom locale
You also have the option to add your own locale using `defineLocale`:
```vue [app.vue]
<script setup lang="ts">
const locale = defineLocale({
name: 'My custom locale',
code: 'en',
dir: 'ltr',
messages: {
// implement pairs
}
})
</script>
<template>
<UApp :locale="locale">
<NuxtPage />
</UApp>
</template>
```
::tip
Look at the `code` parameter, there you need to pass the iso code of the language. Example:
* `hi` Hindi (language)
* `de-AT`: German (language) as used in Austria (region)
::
### Dynamic locale
To dynamically switch between languages, you can use the [Nuxt I18n](https://i18n.nuxtjs.org/) module.
::steps{level="4"}
#### Install the Nuxt I18n package
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @nuxtjs/i18n@next
```
```bash [yarn]
yarn add @nuxtjs/i18n@next
```
```bash [npm]
npm install @nuxtjs/i18n@next
```
```bash [bun]
bun add @nuxtjs/i18n@next
```
::
#### Add the Nuxt I18n module in your `nuxt.config.ts`{lang="ts-type"}
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: [
'@nuxt/ui',
'@nuxtjs/i18n'
],
i18n: {
locales: [{
code: 'de',
name: 'Deutsch'
}, {
code: 'en',
name: 'English'
}, {
code: 'fr',
name: 'Français'
}]
}
})
```
#### Set the `locale` prop using `useI18n`
```vue [app.vue]
<script setup lang="ts">
import * as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
</script>
<template>
<UApp :locale="locales[locale]">
<NuxtPage />
</UApp>
</template>
```
::
### Dynamic direction
Each locale has a `dir` property which will be used by the `App` component to set the directionality of all components.
In a multilingual application, you might want to set the `lang` and `dir` attributes on the `<html>` element dynamically based on the user's locale, which you can do with the [useHead](https://nuxt.com/docs/api/composables/use-head) composable:
```vue [app.vue]
<script setup lang="ts">
import * as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
const lang = computed(() => locales[locale.value].code)
const dir = computed(() => locales[locale.value].dir)
useHead({
htmlAttrs: {
lang,
dir
}
})
</script>
<template>
<UApp :locale="locales[locale]">
<NuxtPage />
</UApp>
</template>
```
## Supported languages
:supported-languages

View File

@@ -1,180 +0,0 @@
---
navigation.title: Vue
title: Internationalization (i18n) in a Vue app
description: 'Learn how to internationalize your Vue app with multi-directional support (LTR/RTL).'
select:
items:
- label: Nuxt
icon: i-logos-nuxt-icon
to: /getting-started/i18n/nuxt
- label: Vue
icon: i-logos-vue
to: /getting-started/i18n/vue
---
## Usage
::note{to="/components/app"}
Nuxt UI provides an [App](/components/app) component that wraps your app to provide global configurations.
::
### Locale
Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:
```vue [App.vue]
<script setup lang="ts">
import { fr } from '@nuxt/ui/locale'
</script>
<template>
<UApp :locale="fr">
<RouterView />
</UApp>
</template>
```
### Custom locale
You also have the option to add your locale using `defineLocale`:
```vue [App.vue]
<script setup lang="ts">
import { defineLocale } from '@nuxt/ui/runtime/composables/defineLocale'
const locale = defineLocale({
name: 'My custom locale',
code: 'en',
dir: 'ltr',
messages: {
// implement pairs
}
})
</script>
<template>
<UApp :locale="locale">
<RouterView />
</UApp>
</template>
```
::tip
Look at the `code` parameter, there you need to pass the iso code of the language. Example:
* `hi` Hindi (language)
* `de-AT`: German (language) as used in Austria (region)
::
### Dynamic locale
To dynamically switch between languages, you can use the [Vue I18n](https://vue-i18n.intlify.dev/) plugin.
::steps{level="4"}
#### Install the Vue I18n package
::code-group{sync="pm"}
```bash [pnpm]
pnpm add vue-i18n@10
```
```bash [yarn]
yarn add vue-i18n@10
```
```bash [npm]
npm install vue-i18n@10
```
```bash [bun]
bun add vue-i18n@10
```
::
#### Use the Vue I18n plugin in your `main.ts`
```ts [main.ts]{2,6-18,22}
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
availableLocales: ['en', 'de'],
messages: {
en: {
// ...
},
de: {
// ...
}
}
})
const app = createApp(App)
app.use(i18n)
app.use(ui)
app.mount('#app')
```
#### Set the `locale` prop using `useI18n`
```vue [App.vue]
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import * as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
</script>
<template>
<UApp :locale="locales[locale]">
<RouterView />
</UApp>
</template>
```
::
### Dynamic direction
Each locale has a `dir` property which will be used by the `App` component to set the directionality of all components.
In a multilingual application, you might want to set the `lang` and `dir` attributes on the `<html>` element dynamically based on the user's locale, which you can do with the [useHead](https://unhead.unjs.io/usage/composables/use-head) composable:
```vue [App.vue]
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useHead } from '@unhead/vue'
import * as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
const lang = computed(() => locales[locale.value].code)
const dir = computed(() => locales[locale.value].dir)
useHead({
htmlAttrs: {
lang,
dir
}
})
</script>
<template>
<UApp :locale="locales[locale]">
<RouterView />
</UApp>
</template>
```
## Supported languages
:supported-languages

View File

@@ -27,10 +27,6 @@ Use it as at the root of your app:
</template>
```
::tip{to="/getting-started/i18n/nuxt#locale"}
Learn how to use the `locale` prop to change the locale of your app.
::
## API
### Props

View File

@@ -67,7 +67,7 @@ class: 'p-8'
### Prev / Next
Use the `prev` and `next` props to customize the prev and next buttons with any [Button](/components/button) props.
Use the `prev` and `next` props to customize the prev and next buttons.
::component-example
---
@@ -76,7 +76,7 @@ class: 'p-8'
---
::
### Prev / Next Icons
### Prev Icon / Next Icon
Use the `prev-icon` and `next-icon` props to customize the buttons [Icon](/components/icon). Defaults to `i-lucide-arrow-left` / `i-lucide-arrow-right`.

View File

@@ -64,7 +64,7 @@ It requires two props:
::
::
Errors are reported directly to the [FormField](/components/form-field) component based on the `name` or `error-pattern` prop. This means the validation rules defined for the `email` attribute in your schema will be applied to `<FormField name="email">`{lang="vue"}.
Errors are reported directly to the [FormField](/components/form-field) component based on the `name` prop. This means the validation rules defined for the `email` attribute in your schema will be applied to `<FormField name="email">`{lang="vue"}.
Nested validation rules are handled using dot notation. For example, a rule like `{ user: z.object({ email: z.string() }) }`{lang="ts"} will be applied to `<FormField name="user.email">`{lang="vue"}.

View File

@@ -214,42 +214,6 @@ props:
---
::
### Create Item
Use the `create-item` prop to allow user input.
::component-code
---
prettier: true
ignore:
- modelValue
- items
external:
- items
- modelValue
items:
createItem:
- true
- 'always'
props:
modelValue: 'Backlog'
items:
- Backlog
- Todo
- In Progress
- Done
createItem: true
---
::
::note
The create option shows when no match is found by default. Set it to `always` to show it even when similar values exist.
::
::tip{to="#emits"}
Use the `@create` event to handle the creation of the item. You will receive the event and the item as arguments.
::
### Content
Use the `content` prop to control how the InputMenu content is rendered, like its `align` or `side` for example.
@@ -730,7 +694,7 @@ This example uses [refDebounced](https://vueuse.org/shared/refDebounced/#refdebo
### With custom search
Use the `filter` prop with an array of fields to filter on. Defaults to `[labelKey]`.
Use the `filter` prop with an array of fields to filter on.
::component-example
---
@@ -739,17 +703,6 @@ name: 'input-menu-filter-fields-example'
---
::
### As a country picker
This example demonstrates using the InputMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
::component-example
---
collapse: true
name: 'input-menu-countries-example'
---
::
## API
### Props

View File

@@ -1,291 +0,0 @@
---
title: InputNumber
description: Input numerical values with a customizable range.
links:
- label: Number Field
icon: i-custom-radix-vue
to: https://www.radix-vue.com/components/number-field
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/InputNumber.vue
navigation.badge: New
---
## Usage
Use the `v-model` directive to control the value of the InputNumber.
::component-code
---
ignore:
- modelValue
external:
- modelValue
props:
modelValue: 5
---
::
Use the `default-value` prop to set the initial value when you do not need to control its state.
::component-code
---
ignore:
- defaultValue
props:
defaultValue: 5
---
::
### Min / Max
Use the `min` and `max` props to set the minimum and maximum values of the InputNumber.
::component-code
---
ignore:
- modelValue
external:
- modelValue
props:
modelValue: 5
min: 0
max: 10
---
::
### Step
Use the `step` prop to set the step value of the InputNumber.
::component-code
---
ignore:
- modelValue
external:
- modelValue
props:
modelValue: 5
step: 2
---
::
### Orientation
Use the `orientation` prop to change the orientation of the InputNumber.
::component-code
---
ignore:
- modelValue
external:
- modelValue
props:
modelValue: 5
orientation: vertical
---
::
### Placeholder
Use the `placeholder` prop to set a placeholder text.
::component-code
---
props:
placeholder: 'Enter a number'
---
::
### Color
Use the `color` prop to change the ring color when the InputNumber is focused.
::component-code
---
ignore:
- modelValue
external:
- modelValue
props:
modelValue: 5
color: neutral
highlight: true
---
::
### Variant
Use the `variant` prop to change the variant of the InputNumber.
::component-code
---
ignore:
- modelValue
external:
- modelValue
props:
modelValue: 5
variant: subtle
color: neutral
highlight: false
---
::
### Size
Use the `size` prop to change the size of the InputNumber.
::component-code
---
ignore:
- modelValue
external:
- modelValue
props:
modelValue: 5
size: xl
---
::
### Disabled
Use the `disabled` prop to disable the InputNumber.
::component-code
---
ignore:
- modelValue
external:
- modelValue
props:
modelValue: 5
disabled: true
---
::
### Increment / Decrement
Use the `increment` and `decrement` props to customize the increment and decrement buttons with any [Button](/components/button) props. Defaults to `{ variant: 'link' }`{lang="ts-type"}.
::component-code
---
prettier: true
ignore:
- modelValue
- increment.size
- increment.color
- increment.variant
- decrement.size
- decrement.color
- decrement.variant
external:
- modelValue
props:
modelValue: 5
increment:
color: neutral
variant: solid
size: xs
decrement:
color: neutral
variant: solid
size: xs
---
::
### Increment / Decrement Icons
Use the `increment-icon` and `decrement-icon` props to customize the buttons [Icon](/components/icon). Defaults to `i-lucide-plus` / `i-lucide-minus`.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: 5
incrementIcon: 'i-lucide-arrow-right'
decrementIcon: 'i-lucide-arrow-left'
---
::
## Examples
### With decimal format
Use the `format-options` prop to customize the format of the value.
::component-example
---
name: 'input-number-decimal-example'
---
::
### With percentage format
Use the `format-options` prop with `style: 'percent'` to customize the format of the value.
::component-example
---
name: 'input-number-percentage-example'
---
::
### With currency format
Use the `format-options` prop with `style: 'currency'` to customize the format of the value.
::component-example
---
name: 'input-number-currency-example'
---
::
### Within a FormField
You can use the InputNumber within a [FormField](/components/form-field) component to display a label, help text, required indicator, etc.
::component-example
---
name: 'input-number-form-field-example'
---
::
### With slots
Use the `#increment` and `#decrement` slots to customize the buttons.
::component-example
---
name: 'input-number-slots-example'
---
::
## API
### Props
:component-props
### Slots
:component-slots
### Emits
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
|----------------------------|-------------------------------------------------|
| `inputRef`{lang="ts-type"} | `Ref<HTMLInputElement \| null>`{lang="ts-type"} |
## Theme
:component-theme

View File

@@ -25,15 +25,15 @@ props:
Use the `type` prop to change the input type. Defaults to `text`.
Some types have been implemented in their own components such as [Checkbox](/components/checkbox), [Radio](/components/radio-group), [InputNumber](/components/input-number) etc. and others have been styled like `file` for example.
Some types have been implemented in their own components such as [Checkbox](/components/checkbox), [Radio](/components/radio-group), etc. and others have been styled like `file` for example.
::component-code
---
items:
type:
- text
- number
- password
- number
- search
- file
props:
@@ -214,16 +214,6 @@ name: 'input-clear-button-example'
---
::
### With copy button
You can put a [Button](/components/button) inside the `#trailing` slot to copy the value to the clipboard.
::component-example
---
name: 'input-copy-button-example'
---
::
### With password toggle
You can put a [Button](/components/button) inside the `#trailing` slot to toggle the password visibility.
@@ -255,20 +245,6 @@ name: 'input-character-limit-example'
---
::
### With keyboard shortcut
You can use the [Kbd](/components/kbd) component inside the `#trailing` slot to add a keyboard shortcut to the Input.
::component-example
---
name: 'input-kbd-example'
---
::
::note{to="/composables/define-shortcuts"}
This example uses the `defineShortcuts` composable to focus the Input when the :kbd{value="/"} key is pressed.
::
### With floating label
You can use the `#default` slot to add a floating label to the Input.

View File

@@ -13,7 +13,6 @@ The Link component is a wrapper around [`<NuxtLink>`](https://nuxt.com/docs/api/
- `inactive-class` prop to set a class when the link is inactive, `active-class` is used when active.
- `exact` prop to style with `active-class` when the link is active and the route is exactly the same as the current route.
- `exact-query` and `exact-hash` props to style with `active-class` when the link is active and the query or hash is exactly the same as the current query or hash.
- use `exact-query="partial"` to style with `active-class` when the link is active and the query partially match the current query.
The incentive behind this is to provide the same API as NuxtLink back in Nuxt 2 / Vue 2. You can read more about it in the Vue Router [migration from Vue 2](https://router.vuejs.org/guide/migration/#removal-of-the-exact-prop-in-router-link) guide.

View File

@@ -137,7 +137,7 @@ Each item can take a `children` array of objects with the following properties t
Use the `orientation` prop to change the orientation of the NavigationMenu.
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children. You can control the open state of each item using the `open` and `defaultOpen` properties.
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children.
::component-code
---
@@ -152,7 +152,6 @@ props:
items:
- - label: Guide
icon: i-lucide-book-open
defaultOpen: true
children:
- label: Introduction
description: Fully styled and customizable components for Nuxt.

View File

@@ -1,182 +0,0 @@
---
title: PinInput
description: An input element to enter a pin.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/PinInput.vue
navigation.badge: New
---
## Usage
Use the `v-model` directive to control the value of the PinInput.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: []
---
::
Use the `default-value` prop to set the initial value when you do not need to control its state.
::component-code
---
prettier: true
ignore:
- defaultValue
props:
defaultValue: ['1','2','3']
---
::
### Type
Use the `type` prop to change the input type. Defaults to `text`.
::component-code
---
items:
type:
- text
- number
props:
type: 'number'
---
::
::note
When `type` is set to `number`, it will only accept numeric characters.
::
### Mask
Use the `mask` prop to treat the input like a password.
::component-code
---
prettier: true
ignore:
- placeholder
- defaultValue
props:
mask: true
defaultValue: ['1','2','3','4','5']
---
::
### OTP
Use the `otp` prop to enable One-Time Password functionality. When enabled, mobile devices can automatically detect and fill OTP codes from SMS messages or clipboard content, with autocomplete support.
::component-code
---
props:
otp: true
---
::
### Length
Use the `length` prop to change the amount of inputs.
::component-code
---
props:
length: 6
---
::
### Placeholder
Use the `placeholder` prop to set a placeholder text.
::component-code
---
props:
placeholder: '○'
---
::
### Color
Use the `color` prop to change the ring color when the PinInput is focused.
::component-code
---
ignore:
- placeholder
props:
color: neutral
highlight: true
placeholder: '○'
---
::
::note
The `highlight` prop is used here to show the focus state. It's used internally when a validation error occurs.
::
### Variant
Use the `variant` prop to change the variant of the PinInput.
::component-code
---
ignore:
- placeholder
props:
color: neutral
variant: subtle
highlight: false
placeholder: '○'
---
::
### Size
Use the `size` prop to change the size of the PinInput.
::component-code
---
ignore:
- placeholder
props:
size: xl
placeholder: '○'
---
::
### Disabled
Use the `disabled` prop to disable the PinInput.
::component-code
---
ignore:
- placeholder
props:
disabled: true
placeholder: '○'
---
::
## API
### Props
:component-props
### Emits
:component-emits
## Theme
:component-theme

View File

@@ -200,9 +200,7 @@ props:
### Search Input
Use the `search-input` prop to customize or hide the search input (with `false` value).
You can pass all the props of the [Input](/components/input) component to customize it.
Use the `search-input` prop to customize the search input. Defaults to `{ placeholder: 'Search...' }`{lang="ts-type"}.
::component-code
---
@@ -221,7 +219,6 @@ props:
icon: 'i-lucide-circle-help'
searchInput:
placeholder: 'Filter...'
icon: 'i-lucide-search'
items:
- label: Backlog
icon: 'i-lucide-circle-help'
@@ -239,44 +236,6 @@ props:
You can set the `search-input` prop to `false` to hide the search input.
::
### Create Item
Use the `create-item` prop to allow user input.
::component-code
---
prettier: true
ignore:
- modelValue
- items
- class
external:
- items
- modelValue
items:
createItem:
- true
- 'always'
props:
modelValue: 'Backlog'
createItem: true
items:
- Backlog
- Todo
- In Progress
- Done
class: 'w-48'
---
::
::note
The create option shows when no match is found by default. Set it to `always` to show it even when similar values exist.
::
::tip{to="#emits"}
Use the `@create` event to handle the creation of the item. You will receive the event and the item as arguments.
::
### Content
Use the `content` prop to control how the SelectMenu content is rendered, like its `align` or `side` for example.
@@ -773,7 +732,7 @@ This example uses [refDebounced](https://vueuse.org/shared/refDebounced/#refdebo
### With custom search
Use the `filter` prop with an array of fields to filter on. Defaults to `[labelKey]`.
Use the `filter` prop with an array of fields to filter on.
::component-example
---
@@ -782,17 +741,6 @@ name: 'select-menu-filter-fields-example'
---
::
### As a country picker
This example demonstrates using the SelectMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
::component-example
---
collapse: true
name: 'select-menu-countries-example'
---
::
## API
### Props

View File

@@ -1,4 +1,5 @@
import { createResolver } from '@nuxt/kit'
import module from '../src/module'
import pkg from '../package.json'
const { resolve } = createResolver(import.meta.url)
@@ -9,7 +10,7 @@ export default defineNuxtConfig({
// ],
modules: [
'../src/module',
module,
'@nuxt/ui-pro',
'@nuxt/content',
'@nuxt/image',
@@ -56,7 +57,6 @@ export default defineNuxtConfig({
routeRules: {
'/': { redirect: '/getting-started', prerender: false },
'/getting-started/installation': { redirect: '/getting-started/installation/nuxt', prerender: false },
'/getting-started/i18n': { redirect: '/getting-started/i18n/nuxt', prerender: false },
'/composables': { redirect: '/composables/define-shortcuts', prerender: false },
'/components': { redirect: '/components/app', prerender: false }
},
@@ -70,8 +70,7 @@ export default defineNuxtConfig({
nitro: {
prerender: {
routes: [
'/getting-started',
'/api/countries.json'
'/getting-started'
// '/api/releases.json',
// '/api/pulls.json'
],

View File

@@ -3,16 +3,15 @@
"name": "@nuxt/ui-docs",
"type": "module",
"dependencies": {
"@iconify-json/logos": "^1.2.3",
"@iconify-json/lucide": "^1.2.15",
"@iconify-json/lucide": "^1.2.12",
"@iconify-json/simple-icons": "^1.2.11",
"@iconify-json/vscode-icons": "^1.2.2",
"@nuxt/content": "3.0.0-alpha.6",
"@nuxt/content": "3.0.0-alpha.5",
"@nuxt/image": "^1.8.1",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@574082c",
"@nuxthub/core": "^0.8.7",
"@nuxtjs/plausible": "^1.1.1",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@62862c8",
"@nuxthub/core": "^0.8.6",
"@nuxtjs/plausible": "^1.0.3",
"@octokit/rest": "^21.0.2",
"@vueuse/nuxt": "^11.2.0",
"joi": "^17.13.3",
@@ -28,6 +27,6 @@
"zod": "^3.23.8"
},
"devDependencies": {
"wrangler": "^3.87.0"
"wrangler": "^3.85.0"
}
}

View File

@@ -1,202 +0,0 @@
type Country = {
name: string
code: string
emoji: string
}
const countries: Country[] = [
{ name: 'Afghanistan', code: 'AF', emoji: '🇦🇫' },
{ name: 'Albania', code: 'AL', emoji: '🇦🇱' },
{ name: 'Algeria', code: 'DZ', emoji: '🇩🇿' },
{ name: 'Andorra', code: 'AD', emoji: '🇦🇩' },
{ name: 'Angola', code: 'AO', emoji: '🇦🇴' },
{ name: 'Antigua and Barbuda', code: 'AG', emoji: '🇦🇬' },
{ name: 'Argentina', code: 'AR', emoji: '🇦🇷' },
{ name: 'Armenia', code: 'AM', emoji: '🇦🇲' },
{ name: 'Australia', code: 'AU', emoji: '🇦🇺' },
{ name: 'Austria', code: 'AT', emoji: '🇦🇹' },
{ name: 'Azerbaijan', code: 'AZ', emoji: '🇦🇿' },
{ name: 'Bahamas', code: 'BS', emoji: '🇧🇸' },
{ name: 'Bahrain', code: 'BH', emoji: '🇧🇭' },
{ name: 'Bangladesh', code: 'BD', emoji: '🇧🇩' },
{ name: 'Barbados', code: 'BB', emoji: '🇧🇧' },
{ name: 'Belarus', code: 'BY', emoji: '🇧🇾' },
{ name: 'Belgium', code: 'BE', emoji: '🇧🇪' },
{ name: 'Belize', code: 'BZ', emoji: '🇧🇿' },
{ name: 'Benin', code: 'BJ', emoji: '🇧🇯' },
{ name: 'Bhutan', code: 'BT', emoji: '🇧🇹' },
{ name: 'Bolivia', code: 'BO', emoji: '🇧🇴' },
{ name: 'Bosnia and Herzegovina', code: 'BA', emoji: '🇧🇦' },
{ name: 'Botswana', code: 'BW', emoji: '🇧🇼' },
{ name: 'Brazil', code: 'BR', emoji: '🇧🇷' },
{ name: 'Brunei', code: 'BN', emoji: '🇧🇳' },
{ name: 'Bulgaria', code: 'BG', emoji: '🇧🇬' },
{ name: 'Burkina Faso', code: 'BF', emoji: '🇧🇫' },
{ name: 'Burundi', code: 'BI', emoji: '🇧🇮' },
{ name: 'Cambodia', code: 'KH', emoji: '🇰🇭' },
{ name: 'Cameroon', code: 'CM', emoji: '🇨🇲' },
{ name: 'Canada', code: 'CA', emoji: '🇨🇦' },
{ name: 'Cape Verde', code: 'CV', emoji: '🇨🇻' },
{ name: 'Central African Republic', code: 'CF', emoji: '🇨🇫' },
{ name: 'Chad', code: 'TD', emoji: '🇹🇩' },
{ name: 'Chile', code: 'CL', emoji: '🇨🇱' },
{ name: 'China', code: 'CN', emoji: '🇨🇳' },
{ name: 'Colombia', code: 'CO', emoji: '🇨🇴' },
{ name: 'Comoros', code: 'KM', emoji: '🇰🇲' },
{ name: 'Congo', code: 'CG', emoji: '🇨🇬' },
{ name: 'Costa Rica', code: 'CR', emoji: '🇨🇷' },
{ name: 'Croatia', code: 'HR', emoji: '🇭🇷' },
{ name: 'Cuba', code: 'CU', emoji: '🇨🇺' },
{ name: 'Cyprus', code: 'CY', emoji: '🇨🇾' },
{ name: 'Czech Republic', code: 'CZ', emoji: '🇨🇿' },
{ name: 'Denmark', code: 'DK', emoji: '🇩🇰' },
{ name: 'Djibouti', code: 'DJ', emoji: '🇩🇯' },
{ name: 'Dominica', code: 'DM', emoji: '🇩🇲' },
{ name: 'Dominican Republic', code: 'DO', emoji: '🇩🇴' },
{ name: 'East Timor', code: 'TL', emoji: '🇹🇱' },
{ name: 'Ecuador', code: 'EC', emoji: '🇪🇨' },
{ name: 'Egypt', code: 'EG', emoji: '🇪🇬' },
{ name: 'El Salvador', code: 'SV', emoji: '🇸🇻' },
{ name: 'Equatorial Guinea', code: 'GQ', emoji: '🇬🇶' },
{ name: 'Eritrea', code: 'ER', emoji: '🇪🇷' },
{ name: 'Estonia', code: 'EE', emoji: '🇪🇪' },
{ name: 'Ethiopia', code: 'ET', emoji: '🇪🇹' },
{ name: 'Fiji', code: 'FJ', emoji: '🇫🇯' },
{ name: 'Finland', code: 'FI', emoji: '🇫🇮' },
{ name: 'France', code: 'FR', emoji: '🇫🇷' },
{ name: 'Gabon', code: 'GA', emoji: '🇬🇦' },
{ name: 'Gambia', code: 'GM', emoji: '🇬🇲' },
{ name: 'Georgia', code: 'GE', emoji: '🇬🇪' },
{ name: 'Germany', code: 'DE', emoji: '🇩🇪' },
{ name: 'Ghana', code: 'GH', emoji: '🇬🇭' },
{ name: 'Greece', code: 'GR', emoji: '🇬🇷' },
{ name: 'Grenada', code: 'GD', emoji: '🇬🇩' },
{ name: 'Guatemala', code: 'GT', emoji: '🇬🇹' },
{ name: 'Guinea', code: 'GN', emoji: '🇬🇳' },
{ name: 'Guinea-Bissau', code: 'GW', emoji: '🇬🇼' },
{ name: 'Guyana', code: 'GY', emoji: '🇬🇾' },
{ name: 'Haiti', code: 'HT', emoji: '🇭🇹' },
{ name: 'Honduras', code: 'HN', emoji: '🇭🇳' },
{ name: 'Hungary', code: 'HU', emoji: '🇭🇺' },
{ name: 'Iceland', code: 'IS', emoji: '🇮🇸' },
{ name: 'India', code: 'IN', emoji: '🇮🇳' },
{ name: 'Indonesia', code: 'ID', emoji: '🇮🇩' },
{ name: 'Iran', code: 'IR', emoji: '🇮🇷' },
{ name: 'Iraq', code: 'IQ', emoji: '🇮🇶' },
{ name: 'Ireland', code: 'IE', emoji: '🇮🇪' },
{ name: 'Israel', code: 'IL', emoji: '🇮🇱' },
{ name: 'Italy', code: 'IT', emoji: '🇮🇹' },
{ name: 'Jamaica', code: 'JM', emoji: '🇯🇲' },
{ name: 'Japan', code: 'JP', emoji: '🇯🇵' },
{ name: 'Jordan', code: 'JO', emoji: '🇯🇴' },
{ name: 'Kazakhstan', code: 'KZ', emoji: '🇰🇿' },
{ name: 'Kenya', code: 'KE', emoji: '🇰🇪' },
{ name: 'Kiribati', code: 'KI', emoji: '🇰🇷' },
{ name: 'Kuwait', code: 'KW', emoji: '🇰🇼' },
{ name: 'Kyrgyzstan', code: 'KG', emoji: '🇰🇬' },
{ name: 'Laos', code: 'LA', emoji: '🇱🇦' },
{ name: 'Latvia', code: 'LV', emoji: '🇱🇻' },
{ name: 'Lebanon', code: 'LB', emoji: '🇱🇧' },
{ name: 'Lesotho', code: 'LS', emoji: '🇱🇸' },
{ name: 'Liberia', code: 'LR', emoji: '🇱🇷' },
{ name: 'Libya', code: 'LY', emoji: '🇱🇾' },
{ name: 'Liechtenstein', code: 'LI', emoji: '🇱🇮' },
{ name: 'Lithuania', code: 'LT', emoji: '🇱🇹' },
{ name: 'Luxembourg', code: 'LU', emoji: '🇱🇺' },
{ name: 'Madagascar', code: 'MG', emoji: '🇲🇬' },
{ name: 'Malawi', code: 'MW', emoji: '🇲🇼' },
{ name: 'Malaysia', code: 'MY', emoji: '🇲🇾' },
{ name: 'Maldives', code: 'MV', emoji: '🇲🇻' },
{ name: 'Mali', code: 'ML', emoji: '🇲🇱' },
{ name: 'Malta', code: 'MT', emoji: '🇲🇹' },
{ name: 'Marshall Islands', code: 'MH', emoji: '🇲🇭' },
{ name: 'Mauritania', code: 'MR', emoji: '🇲🇦' },
{ name: 'Mauritius', code: 'MU', emoji: '🇲🇺' },
{ name: 'Mexico', code: 'MX', emoji: '🇲🇽' },
{ name: 'Micronesia', code: 'FM', emoji: '🇫🇲' },
{ name: 'Moldova', code: 'MD', emoji: '🇲🇩' },
{ name: 'Monaco', code: 'MC', emoji: '🇲🇨' },
{ name: 'Mongolia', code: 'MN', emoji: '🇲🇳' },
{ name: 'Montenegro', code: 'ME', emoji: '🇲🇪' },
{ name: 'Morocco', code: 'MA', emoji: '🇲🇦' },
{ name: 'Mozambique', code: 'MZ', emoji: '🇲🇿' },
{ name: 'Myanmar', code: 'MM', emoji: '🇲🇲' },
{ name: 'Namibia', code: 'NA', emoji: '🇳🇦' },
{ name: 'Nauru', code: 'NR', emoji: '🇳🇷' },
{ name: 'Nepal', code: 'NP', emoji: '🇳🇵' },
{ name: 'Netherlands', code: 'NL', emoji: '🇳🇱' },
{ name: 'New Zealand', code: 'NZ', emoji: '🇳🇿' },
{ name: 'Nicaragua', code: 'NI', emoji: '🇳🇮' },
{ name: 'Niger', code: 'NE', emoji: '🇳🇪' },
{ name: 'Nigeria', code: 'NG', emoji: '🇳🇬' },
{ name: 'North Macedonia', code: 'MK', emoji: '🇲🇰' },
{ name: 'Norway', code: 'NO', emoji: '🇳🇴' },
{ name: 'Oman', code: 'OM', emoji: '🇴🇲' },
{ name: 'Pakistan', code: 'PK', emoji: '🇵🇰' },
{ name: 'Palau', code: 'PW', emoji: '🇵🇼' },
{ name: 'Palestine', code: 'PS', emoji: '🇵🇸' },
{ name: 'Panama', code: 'PA', emoji: '🇵🇦' },
{ name: 'Papua New Guinea', code: 'PG', emoji: '🇵🇬' },
{ name: 'Paraguay', code: 'PY', emoji: '🇵🇾' },
{ name: 'Peru', code: 'PE', emoji: '🇵🇪' },
{ name: 'Philippines', code: 'PH', emoji: '🇵🇭' },
{ name: 'Poland', code: 'PL', emoji: '🇵🇱' },
{ name: 'Portugal', code: 'PT', emoji: '🇵🇹' },
{ name: 'Qatar', code: 'QA', emoji: '🇶🇦' },
{ name: 'Romania', code: 'RO', emoji: '🇷🇴' },
{ name: 'Russia', code: 'RU', emoji: '🇷🇺' },
{ name: 'Rwanda', code: 'RW', emoji: '🇷🇼' },
{ name: 'Saint Kitts and Nevis', code: 'KN', emoji: '🇰🇳' },
{ name: 'Saint Lucia', code: 'LC', emoji: '🇱🇨' },
{ name: 'Saint Vincent and the Grenadines', code: 'VC', emoji: '🇻🇨' },
{ name: 'Samoa', code: 'WS', emoji: '🇼🇸' },
{ name: 'San Marino', code: 'SM', emoji: '🇸🇲' },
{ name: 'Sao Tome and Principe', code: 'ST', emoji: '🇸🇹' },
{ name: 'Saudi Arabia', code: 'SA', emoji: '🇸🇦' },
{ name: 'Senegal', code: 'SN', emoji: '🇸🇳' },
{ name: 'Serbia', code: 'RS', emoji: '🇷🇸' },
{ name: 'Seychelles', code: 'SC', emoji: '🇸🇨' },
{ name: 'Sierra Leone', code: 'SL', emoji: '🇸🇱' },
{ name: 'Singapore', code: 'SG', emoji: '🇸🇬' },
{ name: 'Slovakia', code: 'SK', emoji: '🇸🇰' },
{ name: 'Slovenia', code: 'SI', emoji: '🇸🇮' },
{ name: 'Solomon Islands', code: 'SB', emoji: '🇸🇧' },
{ name: 'Somalia', code: 'SO', emoji: '🇸🇴' },
{ name: 'South Africa', code: 'ZA', emoji: '🇿🇦' },
{ name: 'South Korea', code: 'KR', emoji: '🇰🇷' },
{ name: 'South Sudan', code: 'SS', emoji: '🇸🇸' },
{ name: 'Spain', code: 'ES', emoji: '🇪🇸' },
{ name: 'Sri Lanka', code: 'LK', emoji: '🇱🇰' },
{ name: 'Sudan', code: 'SD', emoji: '🇸🇩' },
{ name: 'Suriname', code: 'SR', emoji: '🇸🇷' },
{ name: 'Sweden', code: 'SE', emoji: '🇸🇪' },
{ name: 'Switzerland', code: 'CH', emoji: '🇨🇭' },
{ name: 'Syria', code: 'SY', emoji: '🇸🇾' },
{ name: 'Taiwan', code: 'TW', emoji: '🇹🇼' },
{ name: 'Tajikistan', code: 'TJ', emoji: '🇹🇯' },
{ name: 'Tanzania', code: 'TZ', emoji: '🇹🇿' },
{ name: 'Thailand', code: 'TH', emoji: '🇹🇭' },
{ name: 'Togo', code: 'TG', emoji: '🇹🇬' },
{ name: 'Tonga', code: 'TO', emoji: '🇹🇴' },
{ name: 'Trinidad and Tobago', code: 'TT', emoji: '🇹🇹' },
{ name: 'Tunisia', code: 'TN', emoji: '🇹🇳' },
{ name: 'Turkey', code: 'TR', emoji: '🇹🇷' },
{ name: 'Turkmenistan', code: 'TM', emoji: '🇹🇲' },
{ name: 'Tuvalu', code: 'TV', emoji: '🇹🇻' },
{ name: 'Uganda', code: 'UG', emoji: '🇺🇬' },
{ name: 'Ukraine', code: 'UA', emoji: '🇺🇦' },
{ name: 'United Arab Emirates', code: 'AE', emoji: '🇦🇪' },
{ name: 'United Kingdom', code: 'GB', emoji: '🇬🇧' },
{ name: 'United States', code: 'US', emoji: '🇺🇸' },
{ name: 'Uruguay', code: 'UY', emoji: '🇺🇾' },
{ name: 'Uzbekistan', code: 'UZ', emoji: '🇺🇿' },
{ name: 'Vanuatu', code: 'VU', emoji: '🇻🇺' },
{ name: 'Vatican City', code: 'VA', emoji: '🇻🇦' },
{ name: 'Venezuela', code: 'VE', emoji: '🇻🇪' },
{ name: 'Vietnam', code: 'VN', emoji: '🇻🇳' },
{ name: 'Yemen', code: 'YE', emoji: '🇾🇪' },
{ name: 'Zambia', code: 'ZM', emoji: '🇿🇲' },
{ name: 'Zimbabwe', code: 'ZW', emoji: '🇿🇼' }
]
export default eventHandler(async () => countries)

View File

@@ -1,8 +1,8 @@
{
"name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "3.0.0-alpha.9",
"packageManager": "pnpm@9.13.2",
"version": "3.0.0-alpha.8",
"packageManager": "pnpm@9.12.3",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/ui.git"
@@ -30,11 +30,7 @@
"./vue-plugin": {
"types": "./vue-plugin.d.ts"
},
"./runtime/*": "./dist/runtime/*",
"./locale": {
"types": "./dist/runtime/locale/index.d.ts",
"import": "./dist/runtime/locale/index.js"
}
"./runtime/*": "./dist/runtime/*"
},
"imports": {
"#build/ui/*": "./.nuxt/ui/*.ts"
@@ -55,7 +51,6 @@
"build": "nuxt-module-build build && pnpm devtools:build",
"prepack": "pnpm build",
"dev": "DEV=true nuxi dev playground",
"dev:vue": "DEV=true vite playground-vue",
"dev:build": "nuxi build playground",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare docs && nuxi prepare devtools && vite build playground-vue",
"devtools": "NUXT_UI_DEVTOOLS_LOCAL=true nuxi dev playground",
@@ -69,60 +64,58 @@
"typecheck": "vue-tsc --noEmit && nuxi typecheck playground && nuxi typecheck docs && nuxi typecheck devtools && cd playground-vue && vue-tsc --noEmit",
"test": "vitest",
"test:vue": "vitest -c vitest.vue.config.ts",
"test:vue:build": "vite build playground-vue",
"release": "release-it --preRelease=alpha --npm.tag=next"
},
"dependencies": {
"@iconify/vue": "^4.1.2",
"@internationalized/number": "^3.5.4",
"@nuxt/devtools-kit": "^1.6.0",
"@nuxt/fonts": "^0.10.2",
"@nuxt/icon": "^1.8.1",
"@nuxt/icon": "^1.6.1",
"@nuxt/kit": "^3.14.159",
"@nuxt/schema": "^3.14.159",
"@nuxtjs/color-mode": "^3.5.2",
"@tailwindcss/postcss": "4.0.0-alpha.34",
"@tailwindcss/vite": "4.0.0-alpha.34",
"@tailwindcss/postcss": "4.0.0-alpha.30",
"@tailwindcss/vite": "4.0.0-alpha.30",
"@tanstack/vue-table": "^8.20.5",
"@unhead/vue": "^1.11.11",
"@vueuse/core": "^11.2.0",
"@vueuse/integrations": "^11.2.0",
"consola": "^3.2.3",
"defu": "^6.1.4",
"embla-carousel-auto-height": "^8.4.0",
"embla-carousel-auto-scroll": "^8.4.0",
"embla-carousel-autoplay": "^8.4.0",
"embla-carousel-class-names": "^8.4.0",
"embla-carousel-fade": "^8.4.0",
"embla-carousel-vue": "^8.4.0",
"embla-carousel-auto-height": "^8.3.1",
"embla-carousel-auto-scroll": "^8.3.1",
"embla-carousel-autoplay": "^8.3.1",
"embla-carousel-class-names": "^8.3.1",
"embla-carousel-fade": "^8.3.1",
"embla-carousel-vue": "^8.3.1",
"embla-carousel-wheel-gestures": "^8.0.1",
"fuse.js": "^7.0.0",
"get-port-please": "^3.1.2",
"knitwork": "^1.1.0",
"magic-string": "^0.30.13",
"mlly": "^1.7.3",
"magic-string": "^0.30.12",
"mlly": "^1.7.2",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
"radix-vue": "^1.9.10",
"radix-vue": "^1.9.8",
"scule": "^1.3.0",
"sirv": "^3.0.0",
"tailwind-variants": "^0.3.0",
"tailwindcss": "4.0.0-alpha.34",
"tailwind-variants": "^0.2.1",
"tailwindcss": "4.0.0-alpha.30",
"tinyglobby": "^0.2.10",
"unplugin": "^1.16.0",
"unplugin-auto-import": "^0.18.5",
"unplugin": "^1.15.0",
"unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.4",
"vaul-vue": "^0.2.0"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.7.1",
"@nuxt/eslint-config": "^0.6.1",
"@nuxt/module-builder": "^0.8.4",
"@nuxt/test-utils": "^3.14.4",
"@release-it/conventional-changelog": "^9.0.3",
"@standard-schema/spec": "1.0.0-beta.3",
"@release-it/conventional-changelog": "^9.0.2",
"@standard-schema/spec": "1.0.0-beta.1",
"@vue/test-utils": "^2.4.6",
"embla-carousel": "^8.4.0",
"eslint": "^9.15.0",
"embla-carousel": "^8.3.1",
"eslint": "^9.14.0",
"happy-dom": "^15.7.4",
"joi": "^17.13.3",
"knitwork": "^1.1.0",
@@ -131,7 +124,7 @@
"release-it": "^17.10.0",
"superstruct": "^2.0.2",
"valibot": "^0.42.1",
"vitest": "^2.1.5",
"vitest": "^2.1.4",
"vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.1.10",
"yup": "^1.4.0",
@@ -142,7 +135,6 @@
},
"resolutions": {
"@nuxt/ui": "workspace:*",
"@nuxt/content": "3.0.0-alpha.5",
"happy-dom": "14.12.3",
"rollup": "^4.24.0"
}

View File

@@ -10,15 +10,15 @@
},
"dependencies": {
"@nuxt/ui": "latest",
"vue": "^3.5.13",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.0",
"@vitejs/plugin-vue": "^5.1.4",
"typescript": "^5.6.3",
"unplugin-auto-import": "^0.18.5",
"unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.11",
"vite": "^5.4.10",
"vue-tsc": "^2.1.10"
}
}

View File

@@ -2,14 +2,12 @@
import { splitByCase, upperFirst } from 'scule'
import { useRouter } from 'vue-router'
import { reactive, ref } from 'vue'
import { useColorMode } from '@vueuse/core'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore included for compatibility with Nuxt playground
import { useAppConfig } from '#imports'
const appConfig = useAppConfig()
const mode = useColorMode()
appConfig.toaster = reactive({
position: 'bottom-right' as const,
@@ -45,7 +43,6 @@ const components = [
'modal',
'navigation-menu',
'pagination',
'pin-input',
'popover',
'progress',
'radio-group',
@@ -86,16 +83,6 @@ defineShortcuts({
<UNavigationMenu :items="items" orientation="vertical" class="hidden lg:flex border-e border-[var(--ui-border)] overflow-y-auto w-48 p-4" />
<UNavigationMenu :items="items" orientation="horizontal" class="lg:hidden border-b border-[var(--ui-border)] overflow-x-auto" />
<div class="fixed top-4 right-4 flex items-center gap-2">
<UButton
:icon="mode === 'dark' ? 'i-lucide-moon' : 'i-lucide-sun'"
color="neutral"
variant="ghost"
:aria-label="`Switch to ${mode === 'dark' ? 'light' : 'dark'} mode`"
@click="mode = mode === 'dark' ? 'light' : 'dark'"
/>
</div>
<div class="flex-1 flex flex-col items-center justify-around overflow-y-auto w-full py-12 px-4">
<Suspense>
<RouterView />
@@ -116,7 +103,7 @@ defineShortcuts({
@import "@nuxt/ui";
@theme {
--font-sans: 'Public Sans', sans-serif;
--font-family-sans: 'Public Sans', sans-serif;
--color-green-50: #EFFDF5;
--color-green-100: #D9FBE8;

View File

@@ -1,21 +1,8 @@
<script setup lang="ts">
import { splitByCase, upperFirst } from 'scule'
import { useColorMode } from '#imports'
const router = useRouter()
const appConfig = useAppConfig()
const colorMode = useColorMode()
defineOptions({ inheritAttrs: false })
const isDark = computed({
get() {
return colorMode.value === 'dark'
},
set() {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
const router = useRouter()
const components = [
'accordion',
@@ -38,13 +25,11 @@ const components = [
'form-field',
'input',
'input-menu',
'input-number',
'kbd',
'link',
'modal',
'navigation-menu',
'pagination',
'pin-input',
'popover',
'progress',
'radio-group',
@@ -87,22 +72,6 @@ defineShortcuts({
<UNavigationMenu :items="items" orientation="vertical" class="hidden lg:flex border-e border-[var(--ui-border)] overflow-y-auto w-48 p-4" />
<UNavigationMenu :items="items" orientation="horizontal" class="lg:hidden border-b border-[var(--ui-border)] overflow-x-auto" />
<div class="fixed top-4 right-4 flex items-center gap-2">
<ClientOnly v-if="!colorMode?.forced">
<UButton
:icon="isDark ? 'i-lucide-moon' : 'i-lucide-sun'"
color="neutral"
variant="ghost"
:aria-label="`Switch to ${isDark ? 'light' : 'dark'} mode`"
@click="isDark = !isDark"
/>
<template #fallback>
<div class="size-8" />
</template>
</ClientOnly>
</div>
<div class="flex-1 flex flex-col items-center justify-around overflow-y-auto w-full py-12 px-4">
<NuxtPage />
</div>
@@ -125,7 +94,7 @@ defineShortcuts({
@import "@nuxt/ui";
@theme {
--font-sans: 'Public Sans', sans-serif;
--font-family-sans: 'Public Sans', sans-serif;
--color-green-50: #EFFDF5;
--color-green-100: #D9FBE8;

View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
input: z.string().min(10),
inputMenu: z.any().refine(option => option?.value === 'option-2', {
message: 'Select Option 2'
}),
inputMenuMultiple: z.any().refine(values => !!values?.find((option: any) => option.value === 'option-2'), {
message: 'Include Option 2'
}),
textarea: z.string().min(10),
select: z.string().refine(value => value === 'option-2', {
message: 'Select Option 2'
}),
selectMenu: z.any().refine(option => option?.value === 'option-2', {
message: 'Select Option 2'
}),
selectMenuMultiple: z.any().refine(values => !!values?.find((option: any) => option.value === 'option-2'), {
message: 'Include Option 2'
}),
switch: z.boolean().refine(value => value === true, {
message: 'Toggle me'
}),
checkbox: z.boolean().refine(value => value === true, {
message: 'Check me'
}),
radioGroup: z.string().refine(value => value === 'option-2', {
message: 'Select Option 2'
}),
slider: z.number().max(20, { message: 'Must be less than 20' })
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({})
const form = useTemplateRef('form')
const items = [
{ label: 'Option 1', value: 'option-1' },
{ label: 'Option 2', value: 'option-2' },
{ label: 'Option 3', value: 'option-3' }
]
function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data)
}
</script>
<template>
<UForm ref="form" :state="state" :schema="schema" class="gap-4 flex flex-col w-60" @submit="onSubmit">
<UFormField label="Input" name="input">
<UInput v-model="state.input" placeholder="john@lennon.com" />
</UFormField>
<UFormField label="Textarea" name="textarea">
<UTextarea v-model="state.textarea" />
</UFormField>
<UFormField name="select" label="Select">
<USelect v-model="state.select" class="w-44" :items="items" />
</UFormField>
<UFormField name="selectMenu" label="Select Menu">
<USelectMenu v-model="state.selectMenu" class="w-44" :items="items" />
</UFormField>
<UFormField name="selectMenuMultiple" label="Select Menu (Multiple)">
<USelectMenu v-model="state.selectMenuMultiple" class="w-44" multiple :items="items" />
</UFormField>
<UFormField name="inputMenu" label="Input Menu">
<UInputMenu v-model="state.inputMenu" :items="items" />
</UFormField>
<UFormField name="inputMenuMultiple" label="Input Menu (Multiple)">
<UInputMenu v-model="state.inputMenuMultiple" multiple :items="items" />
</UFormField>
<UFormField name="checkbox">
<UCheckbox v-model="state.checkbox" label="Check me" />
</UFormField>
<UFormField name="radioGroup">
<URadioGroup v-model="state.radioGroup" legend="Radio group" :items="items" />
</UFormField>
<UFormField name="switch">
<USwitch v-model="state.switch" label="Switch me" />
</UFormField>
<UFormField name="slider" label="Slider">
<USlider v-model="state.slider" />
</UFormField>
<div class="flex gap-2">
<UButton color="neutral" type="submit">
Submit
</UButton>
<UButton color="neutral" variant="outline" @click="form?.clear()">
Clear
</UButton>
</div>
</UForm>
</template>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
email: z.string().min(2),
password: z.string().min(8)
})
type Schema = z.output<typeof schema>
const nestedSchema = z.object({
phone: z.string().length(10)
})
type NestedSchema = z.output<typeof nestedSchema>
const state = reactive<Partial<Schema & { nested: Partial<NestedSchema> }>>({
nested: {}
})
const checked = ref(false)
function onSubmit(event: FormSubmitEvent<Schema>) {
console.log('Success', event.data)
}
function onError(event: any) {
console.log('Error', event)
}
</script>
<template>
<UForm
:state="state"
:schema="schema"
class="gap-4 flex flex-col w-60"
@submit="onSubmit"
@error="onError"
>
<UFormField label="Email" name="email">
<UInput v-model="state.email" placeholder="john@lennon.com" />
</UFormField>
<UFormField label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormField>
<div>
<UCheckbox v-model="checked" name="check" label="Check me" @change="state.nested = {}" />
</div>
<UForm v-if="checked && state.nested" :state="state.nested" :schema="nestedSchema">
<UFormField label="Phone" name="phone">
<UInput v-model="state.nested.phone" />
</UFormField>
</UForm>
<div>
<UButton color="neutral" type="submit">
Submit
</UButton>
</div>
</UForm>
</template>

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
email: z.string().min(2),
password: z.string().min(8)
})
type Schema = z.output<typeof schema>
const itemSchema = z.object({
name: z.string().min(1),
price: z.string().min(1)
})
type ItemSchema = z.output<typeof itemSchema>
const state = reactive<Partial<Schema & { items: Partial<ItemSchema>[] }>>({})
function addItem() {
if (!state.items) {
state.items = []
}
state.items.push({})
}
function removeItem() {
if (state.items) {
state.items.pop()
}
}
function onSubmit(event: FormSubmitEvent<Schema>) {
console.log('Success', event.data)
}
function onError(event: any) {
console.log('Error', event)
}
</script>
<template>
<UForm
:state="state"
:schema="schema"
class="gap-4 flex flex-col w-60"
@submit="onSubmit"
@error="onError"
>
<UFormField label="Email" name="email">
<UInput v-model="state.email" placeholder="john@lennon.com" />
</UFormField>
<UFormField label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormField>
<UForm v-for="item, count in state.items" :key="count" :state="item" :schema="itemSchema" class="flex gap-2">
<UFormField label="Name" name="name">
<UInput v-model="item.name" />
</UFormField>
<UFormField label="Price" name="price">
<UInput v-model="item.price" />
</UFormField>
</UForm>
<div class="flex gap-2">
<UButton color="neutral" variant="subtle" size="sm" @click="addItem()">
Add Item
</UButton>
<UButton color="neutral" variant="ghost" size="sm" @click="removeItem()">
Remove Item
</UButton>
</div>
<div>
<UButton color="neutral" type="submit">
Submit
</UButton>
</div>
</UForm>
</template>

View File

@@ -1,9 +1,6 @@
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
import FormExampleElements from '../../../../docs/app/components/content/examples/form/FormExampleElements.vue'
import FormExampleNestedList from '../../../../docs/app/components/content/examples/form/FormExampleNestedList.vue'
import FormExampleNested from '../../../../docs/app/components/content/examples/form/FormExampleNested.vue'
const schema = z.object({
email: z.string().email(),
@@ -11,20 +8,18 @@ const schema = z.object({
tos: z.literal(true)
})
type Schema = z.input<typeof schema>
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({})
const state2 = reactive<Partial<Schema>>({})
function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data)
}
const validateOn = ref(['input', 'change', 'blur'])
const disabled = ref(false)
</script>
<template>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex gap-4">
<UForm
:state="state"
@@ -45,24 +40,75 @@ const disabled = ref(false)
</UFormField>
<div>
<UButton type="submit">
<UButton color="neutral" type="submit">
Submit
</UButton>
</div>
</UForm>
<FormExampleNested />
<FormExampleNestedList />
<UForm
:state="state2"
:schema="schema"
class="gap-4 flex flex-col w-60"
:validate-on-input-delay="2000"
@submit="onSubmit"
>
<UFormField label="Email" name="email">
<UInput v-model="state2.email" placeholder="john@lennon.com" />
</UFormField>
<UFormField
label="Password"
name="password"
:validate-on-input-delay="50"
eager-validation
>
<UInput v-model="state2.password" type="password" />
</UFormField>
<div>
<UButton color="neutral" type="submit">
Submit
</UButton>
</div>
</UForm>
<FormNestedExample />
<FormNestedListExample />
</div>
<div class="border border-[var(--ui-border)] rounded-lg">
<div class="py-2 px-4 flex gap-4 items-center">
<UFormField label="Validate on" class="flex items-center gap-2">
<USelectMenu v-model="validateOn" :items="['input', 'change', 'blur']" multiple class="w-48" />
</UFormField>
<UCheckbox v-model="disabled" label="Disabled" />
</div>
<USeparator class="my-8" />
<FormExampleElements :validate-on="validateOn" :disabled="disabled" class="border-t border-[var(--ui-border)] p-4" />
<div class="flex gap-4 flex-wrap">
<div>
<p class="text-lg font-bold underline mb-4">
Validate on input
</p>
<FormElementsExample :validate-on="['input']" />
</div>
<div>
<p class="text-lg font-bold underline mb-4">
Validate on change
</p>
<FormElementsExample :validate-on="['change']" />
</div>
<div>
<p class="text-lg font-bold underline mb-4">
Validate on blur
</p>
<FormElementsExample :validate-on="['blur']" />
</div>
<div>
<p class="text-lg font-bold underline mb-4">
Default
</p>
<FormElementsExample />
</div>
<div>
<p class="text-lg font-bold underline mb-4">
Disabled
</p>
<FormElementsExample disabled />
</div>
</div>
</div>
</template>

View File

@@ -1,68 +0,0 @@
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 w-48">
<UInputNumber />
</div>
<div class="flex items-center gap-2">
<UInputNumber
v-for="variant in variants"
:key="variant"
:placeholder="upperFirst(variant)"
:variant="variant"
class="w-48"
/>
</div>
<div class="flex items-center gap-2">
<UInputNumber
v-for="variant in variants"
:key="variant"
:placeholder="upperFirst(variant)"
:variant="variant"
color="neutral"
class="w-48"
/>
</div>
<div class="flex items-center gap-2">
<UInputNumber
v-for="variant in variants"
:key="variant"
:placeholder="upperFirst(variant)"
:variant="variant"
color="error"
highlight
class="w-48"
/>
</div>
<div class="flex flex-col gap-4 w-48">
<UInputNumber placeholder="Disabled" disabled />
<UInputNumber placeholder="Required" required />
</div>
<div class="flex items-center gap-4">
<UInputNumber
v-for="size in sizes"
:key="size"
:size="size"
:placeholder="`Horizontal ${size}`"
class="w-48"
/>
</div>
<div class="flex items-center gap-4">
<UInputNumber
v-for="size in sizes"
:key="size"
:size="size"
class="w-48"
:placeholder="`Vertical ${size}`"
orientation="vertical"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { upperFirst } from 'scule'
import theme from '#build/ui/input-number'
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size>
const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme.variants.variant>
</script>

View File

@@ -8,7 +8,7 @@ const orientations = Object.keys(theme.variants.orientation)
const color = ref(theme.defaultVariants.color)
const highlightColor = ref()
const variant = ref(theme.defaultVariants.variant)
const orientation = ref('vertical' as const)
const orientation = ref('horizontal' as const)
const highlight = ref(true)
const items = [
@@ -16,7 +16,6 @@ const items = [
label: 'Documentation',
icon: 'i-lucide-book-open',
badge: 10,
defaultOpen: true,
children: [{
label: 'Introduction',
description: 'Fully styled and customizable components for Nuxt.',

View File

@@ -1,52 +0,0 @@
<script setup lang="ts">
import theme from '#build/ui/pin-input'
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size>
const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme.variants.variant>
const onComplete = (e: string[]) => {
alert(e.join(''))
}
</script>
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex gap-4">
<UPinInput placeholder="○" autofocus @complete="onComplete" />
</div>
<div class="flex items-center gap-4">
<UPinInput v-for="variant in variants" :key="variant" placeholder="○" :variant="variant" />
</div>
<div class="flex items-center gap-4">
<UPinInput
v-for="variant in variants"
:key="variant"
placeholder="○"
:variant="variant"
color="neutral"
/>
</div>
<div class="flex items-center gap-4">
<UPinInput
v-for="variant in variants"
:key="variant"
placeholder="○"
:variant="variant"
color="error"
highlight
/>
</div>
<div class="flex flex-col gap-4">
<UPinInput placeholder="○" disabled />
<UPinInput placeholder="○" required />
</div>
<div class="flex items-center gap-4">
<UPinInput
v-for="size in sizes"
:key="size"
placeholder="○"
:size="size"
/>
</div>
</div>
</template>

View File

@@ -8,8 +8,7 @@
"generate": "nuxi generate"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.15",
"@iconify-json/simple-icons": "^1.2.11",
"@iconify-json/lucide": "^1.2.12",
"@nuxt/ui": "latest",
"nuxt": "^3.14.159"
}

3315
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

36
scripts/bump-edge.ts Normal file
View File

@@ -0,0 +1,36 @@
import { promises as fsp } from 'node:fs'
import { resolve } from 'node:path'
import { execSync } from 'node:child_process'
async function loadPackage(dir: string) {
const pkgPath = resolve(dir, 'package.json')
const data = JSON.parse(await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}'))
const save = () => fsp.writeFile(pkgPath, JSON.stringify(data, null, 2) + '\n')
return {
dir,
data,
save
}
}
async function main() {
const pkg = await loadPackage(process.cwd())
const commit = execSync('git rev-parse --short HEAD').toString('utf-8').trim()
const date = Math.round(Date.now() / (1000 * 60))
pkg.data.name = `${pkg.data.name}-edge`
pkg.data.version = `${pkg.data.version}-${date}.${commit}`
pkg.save()
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

19
scripts/release-edge.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
# Restore all git changes
git restore -s@ -SW -- .
# Bump versions to edge
pnpm jiti ./scripts/bump-edge
# Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
echo "registry=https://registry.npmjs.org/" >> ~/.npmrc
echo "always-auth=true" >> ~/.npmrc
npm whoami
fi
# Release package
echo "Publishing @nuxt/ui"
npm publish -q --access public

16
scripts/release.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
# Restore all git changes
git restore -s@ -SW -- .
# Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
echo "registry=https://registry.npmjs.org/" >> ~/.npmrc
echo "always-auth=true" >> ~/.npmrc
npm whoami
fi
# Release package
echo "Publishing @nuxt/ui"
npm publish -q --access public

View File

@@ -176,18 +176,16 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.routeRules = defu(nuxt.options.routeRules, { '/__nuxt_ui__/**': { ssr: false } })
extendPages((pages) => {
if (pages.length) {
pages.unshift({
name: 'ui-devtools',
path: '/__nuxt_ui__/components/:slug',
file: resolve('./devtools/runtime/DevtoolsRenderer.vue'),
meta: {
// https://github.com/nuxt/nuxt/pull/29366
// isolate: true
layout: false
}
})
}
pages.unshift({
name: 'ui-devtools',
path: '/__nuxt_ui__/components/:slug',
file: resolve('./devtools/runtime/DevtoolsRenderer.vue'),
meta: {
// https://github.com/nuxt/nuxt/pull/29366
// isolate: true
layout: false
}
})
})
addCustomTab({

View File

@@ -28,7 +28,6 @@ export interface AlertProps {
* Display a list of actions:
* - under the title and description if multiline
* - next to the close button if not multiline
* `{ size: 'xs' }`{lang="ts-type"}
*/
actions?: ButtonProps[]
/**
@@ -66,7 +65,6 @@ extendDevtoolsMeta<AlertProps>({ defaultProps: { title: 'Heads up!' } })
import { computed } from 'vue'
import { Primitive } from 'radix-vue'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import UButton from './Button.vue'
@@ -76,7 +74,6 @@ const emits = defineEmits<AlertEmits>()
const slots = defineSlots<AlertSlots>()
const appConfig = useAppConfig()
const { t } = useLocale()
const multiline = computed(() => !!props.title && !!props.description)
@@ -126,7 +123,7 @@ const ui = computed(() => alert({
size="md"
color="neutral"
variant="link"
:aria-label="t('alert.close')"
aria-label="Close"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
@click="emits('update:open', false)"

View File

@@ -1,14 +1,11 @@
<script lang="ts">
import type { ConfigProviderProps, TooltipProviderProps } from 'radix-vue'
import { localeContextInjectionKey } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { ToasterProps, Locale } from '../types'
import { en } from '../locale'
import type { ToasterProps } from '../types'
export interface AppProps extends Omit<ConfigProviderProps, 'useId' | 'dir'> {
export interface AppProps extends Omit<ConfigProviderProps, 'useId'> {
tooltip?: TooltipProviderProps
toaster?: ToasterProps | null
locale?: Locale
}
export interface AppSlots {
@@ -23,7 +20,7 @@ extendDevtoolsMeta({ ignore: true })
</script>
<script setup lang="ts">
import { toRef, useId, provide, computed } from 'vue'
import { toRef, useId } from 'vue'
import { ConfigProvider, TooltipProvider, useForwardProps } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import UToaster from './Toaster.vue'
@@ -33,16 +30,13 @@ import USlideoverProvider from './SlideoverProvider.vue'
const props = defineProps<AppProps>()
defineSlots<AppSlots>()
const configProviderProps = useForwardProps(reactivePick(props, 'scrollBody'))
const configProviderProps = useForwardProps(reactivePick(props, 'dir', 'scrollBody'))
const tooltipProps = toRef(() => props.tooltip)
const toasterProps = toRef(() => props.toaster)
const locale = computed(() => props.locale || en)
provide(localeContextInjectionKey, locale)
</script>
<template>
<ConfigProvider :use-id="() => (useId() as string)" :dir="locale.dir" v-bind="configProviderProps">
<ConfigProvider :use-id="() => (useId() as string)" v-bind="configProviderProps">
<TooltipProvider v-bind="tooltipProps">
<UToaster v-if="toaster !== null" v-bind="toasterProps">
<slot />

View File

@@ -21,7 +21,7 @@ export interface BreadcrumbItem extends Omit<LinkProps, 'raw' | 'custom'> {
export interface BreadcrumbProps<T> {
/**
* The element or component this component should render as.
* @defaultValue 'nav'
* @defaultValue 'div'
*/
as?: any
items?: T[]
@@ -76,10 +76,8 @@ extendDevtoolsMeta({
</script>
<script setup lang="ts" generic="T extends BreadcrumbItem">
import { computed } from 'vue'
import { Primitive } from 'radix-vue'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import { get } from '../utils'
import { pickLinkProps } from '../utils/link'
import UIcon from './Icon.vue'
@@ -88,14 +86,11 @@ import ULinkBase from './LinkBase.vue'
import ULink from './Link.vue'
const props = withDefaults(defineProps<BreadcrumbProps<T>>(), {
as: 'nav',
labelKey: 'label'
})
const slots = defineSlots<BreadcrumbSlots<T>>()
const { dir } = useLocale()
const appConfig = useAppConfig()
const separatorIcon = computed(() => props.separatorIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronLeft : appConfig.ui.icons.chevronRight))
const appConfig = useAppConfig()
// eslint-disable-next-line vue/no-dupe-keys
const ui = breadcrumb()
@@ -128,7 +123,7 @@ const ui = breadcrumb()
<li v-if="index < items!.length - 1" role="presentation" :class="ui.separator({ class: props.ui?.separator })">
<slot name="separator">
<UIcon :name="separatorIcon" :class="ui.separatorIcon({ class: props.ui?.separatorIcon })" />
<UIcon :name="separatorIcon || appConfig.ui.icons.chevronRight" :class="ui.separatorIcon({ class: props.ui?.separatorIcon })" />
</slot>
</li>
</template>

View File

@@ -100,7 +100,6 @@ import useEmblaCarousel from 'embla-carousel-vue'
import { useForwardProps } from 'radix-vue'
import { reactivePick, computedAsync } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import UButton from './Button.vue'
const props = withDefaults(defineProps<CarouselProps<T>>(), {
@@ -135,12 +134,8 @@ const props = withDefaults(defineProps<CarouselProps<T>>(), {
defineSlots<CarouselSlots<T>>()
const appConfig = useAppConfig()
const { dir, t } = useLocale()
const rootProps = useForwardProps(reactivePick(props, 'active', 'align', 'breakpoints', 'containScroll', 'dragFree', 'dragThreshold', 'duration', 'inViewThreshold', 'loop', 'skipSnaps', 'slidesToScroll', 'startIndex', 'watchDrag', 'watchResize', 'watchSlides', 'watchFocus'))
const prevIcon = computed(() => props.prevIcon || (dir.value === 'rtl' ? appConfig.ui.icons.arrowRight : appConfig.ui.icons.arrowLeft))
const nextIcon = computed(() => props.nextIcon || (dir.value === 'rtl' ? appConfig.ui.icons.arrowLeft : appConfig.ui.icons.arrowRight))
const ui = computed(() => carousel({
orientation: props.orientation
}))
@@ -149,7 +144,8 @@ const options = computed<EmblaOptionsType>(() => ({
...(props.fade ? { align: 'center', containScroll: false } : {}),
...rootProps.value,
axis: props.orientation === 'horizontal' ? 'x' : 'y',
direction: dir.value === 'rtl' ? 'rtl' : 'ltr'
// TODO: Get from ConfigProvider
direction: 'ltr'
}))
const plugins = computedAsync<EmblaPluginType[]>(async () => {
@@ -279,22 +275,22 @@ defineExpose({
<div v-if="arrows" :class="ui.arrows({ class: props.ui?.arrows })">
<UButton
:disabled="!canScrollPrev"
:icon="prevIcon"
:icon="prevIcon || appConfig.ui.icons.arrowLeft"
size="md"
color="neutral"
variant="outline"
:aria-label="t('carousel.prev')"
aria-label="Prev"
v-bind="typeof prev === 'object' ? prev : undefined"
:class="ui.prev({ class: props.ui?.prev })"
@click="scrollPrev"
/>
<UButton
:disabled="!canScrollNext"
:icon="nextIcon"
:icon="nextIcon || appConfig.ui.icons.arrowRight"
size="md"
color="neutral"
variant="outline"
:aria-label="t('carousel.next')"
aria-label="Next"
v-bind="typeof next === 'object' ? next : undefined"
:class="ui.next({ class: props.ui?.next })"
@click="scrollNext"
@@ -304,7 +300,7 @@ defineExpose({
<div v-if="dots" :class="ui.dots({ class: props.ui?.dots })">
<template v-for="(_, index) in scrollSnaps" :key="index">
<button
:aria-label="t('carousel.goto', { slide: index + 1 })"
:aria-label="`Go to slide ${index + 1}`"
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
@click="scrollTo(index)"
/>

View File

@@ -35,7 +35,7 @@ export interface CommandPaletteGroup<T> {
slot?: string
items?: T[]
/**
* Whether to filter group items with [useFuse](https://vueuse.org/integrations/useFuse).
* Wether to filter group items with [useFuse](https://vueuse.org/integrations/useFuse).
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
* @defaultValue true
*/
@@ -124,7 +124,6 @@ import { defu } from 'defu'
import { reactivePick } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import { omit, get } from '../utils'
import { highlight } from '../utils/fuse'
import UIcon from './Icon.vue'
@@ -145,7 +144,6 @@ const slots = defineSlots<CommandPaletteSlots<G, T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'selectedValue', 'resetSearchTermOnBlur'), emits)
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon', 'placeholder'))
@@ -247,7 +245,7 @@ const groups = computed(() => {
size="md"
color="neutral"
variant="ghost"
:aria-label="t('commandPalette.close')"
aria-label="Close"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
@click="emits('update:open', false)"
@@ -261,7 +259,7 @@ const groups = computed(() => {
<ComboboxContent :class="ui.content({ class: props.ui?.content })" :dismissable="false">
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? t('commandPalette.noMatch', { searchTerm }) : t('commandPalette.noData') }}
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
</slot>
</ComboboxEmpty>

View File

@@ -34,8 +34,6 @@ import ULink from './Link.vue'
import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue'
import UKbd from './Kbd.vue'
// eslint-disable-next-line import/no-self-import
import UContextMenuContent from './ContextMenuContent.vue'
const props = defineProps<ContextMenuContentProps<T>>()
const emits = defineEmits<ContextMenuContentEmits>()

View File

@@ -40,8 +40,6 @@ import ULink from './Link.vue'
import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue'
import UKbd from './Kbd.vue'
// eslint-disable-next-line import/no-self-import
import UDropdownMenuContent from './DropdownMenuContent.vue'
const props = defineProps<DropdownMenuContentProps<T>>()
const emits = defineEmits<DropdownMenuContentEmits>()

View File

@@ -38,7 +38,7 @@ extendDevtoolsMeta({ example: 'FormExample' })
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
import { useEventBus } from '@vueuse/core'
import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField'
import { parseSchema } from '../utils/form'
import { getYupErrors, isYupSchema, getValibotErrors, isValibotSchema, getZodErrors, isZodSchema, getJoiErrors, isJoiSchema, getStandardErrors, isStandardSchema, getSuperStructErrors, isSuperStructSchema } from '../utils/form'
import { FormValidationException } from '../types/form'
const props = withDefaults(defineProps<FormProps<T>>(), {
@@ -94,13 +94,13 @@ onUnmounted(() => {
const errors = ref<FormErrorWithId[]>([])
provide('form-errors', errors)
const inputs = ref<Record<string, { id?: string, pattern?: RegExp }>>({})
const inputs = ref<Record<string, string>>({})
provide(formInputsInjectionKey, inputs)
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
return errs.map(err => ({
...err,
id: inputs.value[err.name]?.id
id: inputs.value[err.name]
}))
}
@@ -108,11 +108,20 @@ async function getErrors(): Promise<FormErrorWithId[]> {
let errs = props.validate ? (await props.validate(props.state)) ?? [] : []
if (props.schema) {
const { errors, result } = await parseSchema(props.state, props.schema as FormSchema<typeof props.state>)
if (errors) {
errs = errs.concat(errors)
if (isZodSchema(props.schema)) {
errs = errs.concat(await getZodErrors(props.state, props.schema))
} else if (isYupSchema(props.schema)) {
errs = errs.concat(await getYupErrors(props.state, props.schema))
} else if (isJoiSchema(props.schema)) {
errs = errs.concat(await getJoiErrors(props.state, props.schema))
} else if (isValibotSchema(props.schema)) {
errs = errs.concat(await getValibotErrors(props.state, props.schema))
} else if (isSuperStructSchema(props.schema)) {
errs = errs.concat(await getSuperStructErrors(props.state, props.schema))
} else if (isStandardSchema(props.schema)) {
errs = errs.concat(await getStandardErrors(props.state, props.schema))
} else {
Object.assign(props.state, result)
throw new Error('Form validation failed: Unsupported form schema')
}
}
@@ -120,7 +129,7 @@ async function getErrors(): Promise<FormErrorWithId[]> {
}
async function _validate(opts: { name?: string | string[], silent?: boolean, nested?: boolean } = { silent: false, nested: true }): Promise<T | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as string[]
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name
const nestedValidatePromises = !names && opts.nested
? Array.from(nestedForms.value.values()).map(
@@ -134,16 +143,9 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes
: []
if (names) {
const otherErrors = errors.value.filter(error => !names.some((name) => {
const pattern = inputs.value?.[name]?.pattern
return name === error.name || (pattern && error.name.match(pattern))
}))
const pathErrors = (await getErrors()).filter(error => names.some((name) => {
const pattern = inputs.value?.[name]?.pattern
return name === error.name || (pattern && error.name.match(pattern))
}))
const otherErrors = errors.value.filter(error => !names!.includes(error.name))
const pathErrors = (await getErrors()).filter(error => names!.includes(error.name)
)
errors.value = otherErrors.concat(pathErrors)
} else {
errors.value = await getErrors()
@@ -194,7 +196,7 @@ provide(formOptionsInjectionKey, computed(() => ({
validateOnInputDelay: props.validateOnInputDelay
})))
defineExpose<Form<T>>({
defineExpose<{ $el: HTMLFormElement | HTMLDivElement } & Form<T>>({
validate: _validate,
errors,
@@ -228,7 +230,7 @@ defineExpose<Form<T>>({
},
disabled
})
} as { $el: HTMLFormElement | HTMLDivElement } & Form<T>)
</script>
<template>

View File

@@ -12,10 +12,7 @@ const formField = tv({ extend: tv(theme), ...(appConfig.ui?.formField || {}) })
type FormFieldVariants = VariantProps<typeof formField>
export interface FormFieldProps {
/** The name of the FormField. Also used to match form errors. */
name?: string
/** A regular expression to match form error names. */
errorPattern?: RegExp
label?: string
description?: string
help?: string
@@ -57,7 +54,7 @@ const ui = computed(() => formField({
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern)))?.message)
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name)?.message)
const id = ref(useId())
@@ -68,8 +65,7 @@ provide(formFieldInjectionKey, computed(() => ({
name: props.name,
size: props.size,
eagerValidation: props.eagerValidation,
validateOnInputDelay: props.validateOnInputDelay,
errorPattern: props.errorPattern
validateOnInputDelay: props.validateOnInputDelay
}) as FormFieldInjectedOptions<FormFieldProps>))
</script>
@@ -96,7 +92,7 @@ provide(formFieldInjectionKey, computed(() => ({
</p>
</div>
<div :class="[(label || !!slots.label || description || !!slots.description) && ui.container({ class: props.ui?.container })]">
<div :class="[label && ui.container({ class: props.ui?.container })]">
<slot :error="error" />
<p v-if="(typeof error === 'string' && error) || !!slots.error" :class="ui.error({ class: props.ui?.error })">

View File

@@ -78,10 +78,9 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
*/
portal?: boolean
/**
* Whether to filter items or not, can be an array of fields to filter. Defaults to `[labelKey]`.
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
* `['label']`{lang="ts-type"}
* @defaultValue true
* Whether to filter items or not, can be an array of fields to filter.
* When `false`, items will not be filtered which is useful for custom filtering.
* @defaultValue ['label']
*/
filter?: boolean | string[]
/**
@@ -97,11 +96,6 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
items?: I
/** Highlight the ring color like a focus state. */
highlight?: boolean
/**
* Determines if custom user input that does not exist in options can be added.
* @defaultValue false
*/
createItem?: boolean | 'always' | { placement?: 'top' | 'bottom', when?: 'empty' | 'always' }
class?: any
ui?: PartialString<typeof inputMenu.slots>
/** The controlled value of the Combobox. Can be binded-with with `v-model`. */
@@ -114,7 +108,6 @@ export type InputMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>,
change: [payload: Event]
blur: [payload: FocusEvent]
focus: [payload: FocusEvent]
create: [payload: Event, item: T]
} & SelectModelValueEmits<T, V, M>
type SlotProps<T> = (props: { item: T, index: number }) => any
@@ -129,23 +122,21 @@ export interface InputMenuSlots<T> {
'item-trailing': SlotProps<T>
'tags-item-text': SlotProps<T>
'tags-item-delete': SlotProps<T>
'create-item-label'(props: { item: T }): any
}
extendDevtoolsMeta({ defaultProps: { items: ['Option 1', 'Option 2', 'Option 3'] } })
</script>
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<InputMenuItem | AcceptableValue> = MaybeArrayOfArray<InputMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
import { computed, ref, toRef, onMounted, toRaw } from 'vue'
import { computed, ref, toRef, onMounted } from 'vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits } from 'radix-vue'
import { defu } from 'defu'
import { isEqual } from 'ohash'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import { get, escapeRegExp } from '../utils'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
@@ -157,7 +148,7 @@ const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
type: 'text',
autofocusDelay: 0,
portal: true,
filter: true,
filter: () => ['label'],
labelKey: 'label' as never
})
const emits = defineEmits<InputMenuEmits<T, V, M>>()
@@ -166,7 +157,6 @@ const slots = defineSlots<InputMenuSlots<T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'resetSearchTermOnBlur'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
@@ -177,8 +167,6 @@ const { emitFormBlur, emitFormChange, emitFormInput, size: formGroupSize, color,
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate()
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
const ui = computed(() => inputMenu({
@@ -203,27 +191,23 @@ function displayValue(value: T): string {
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
}
function filterFunction(
inputItems: ArrayOrWrapped<T> = items.value as ArrayOrWrapped<T>,
filterSearchTerm: string = searchTerm.value,
comparator = (item: any, term: string) => String(item).search(new RegExp(term, 'i')) !== -1
): ArrayOrWrapped<T> {
function filterFunction(items: ArrayOrWrapped<T>, searchTerm: string): ArrayOrWrapped<T> {
if (props.filter === false) {
return inputItems
return items
}
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
const escapedSearchTerm = escapeRegExp(filterSearchTerm ?? '')
const escapedSearchTerm = escapeRegExp(searchTerm)
return inputItems.filter((item) => {
return items.filter((item) => {
if (typeof item !== 'object') {
return comparator(item, escapedSearchTerm)
return String(item).search(new RegExp(escapedSearchTerm, 'i')) !== -1
}
return fields.some((field) => {
const child = get(item, field as string)
return child !== null && child !== undefined && comparator(child, escapedSearchTerm)
return child !== null && child !== undefined && String(child).search(new RegExp(escapedSearchTerm, 'i')) !== -1
})
}) as ArrayOrWrapped<T>
}
@@ -232,36 +216,6 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
// eslint-disable-next-line vue/no-dupe-keys
const items = computed(() => groups.value.flatMap(group => group) as T[])
const creatable = computed(() => {
if (!props.createItem) {
return false
}
const isModelValueCustom = props.modelValue && filterFunction((props.multiple && Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue]) as ArrayOrWrapped<T>, searchTerm.value, (item, term) => String(item) === term).length === 1
if (isModelValueCustom) {
return false
}
const filteredItems = filterFunction()
const newItem = searchTerm.value && {
item: props.valueKey ? { [props.valueKey]: searchTerm.value, [props.labelKey ?? 'label']: searchTerm.value } : searchTerm.value,
position: ((typeof props.createItem === 'object' && props.createItem.placement) || 'bottom') as 'top' | 'bottom'
}
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
return (filteredItems.length === 1 && filterFunction(filteredItems, searchTerm.value, (item, term) => String(item) === term).length === 1) ? false : newItem
}
return filteredItems.length > 0 ? false : newItem
})
const rootItems = computed(() => [
...(creatable.value && creatable.value.position === 'top' ? [creatable.value.item] : []),
...filterFunction(),
...(creatable.value && creatable.value.position === 'bottom' ? [creatable.value.item] : [])
] as ArrayOrWrapped<T>)
const inputRef = ref<InstanceType<typeof ComboboxInput> | null>(null)
function autoFocus() {
@@ -277,9 +231,6 @@ onMounted(() => {
})
function onUpdate(value: any) {
if (toRaw(props.modelValue) === value) {
return
}
// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
@@ -313,22 +264,6 @@ defineExpose({
</script>
<template>
<DefineCreateItemTemplate>
<ComboboxGroup v-if="creatable" :class="ui.group({ class: props.ui?.group })">
<ComboboxItem
:class="ui.item({ class: props.ui?.item })"
:value="valueKey && typeof creatable.item === 'object' ? get(creatable.item, props.valueKey as string) : creatable.item"
@select="e => emits('create', e, (creatable as any).item as T)"
>
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
<slot name="create-item-label" :item="(creatable.item as T)">
{{ t('inputMenu.create', { label: typeof creatable.item === 'object' ? get(creatable.item, props.labelKey as string) : creatable.item }) }}
</slot>
</span>
</ComboboxItem>
</ComboboxGroup>
</DefineCreateItemTemplate>
<ComboboxRoot
:id="id"
v-slot="{ modelValue, open }"
@@ -338,7 +273,7 @@ defineExpose({
:disabled="disabled"
:multiple="multiple"
:display-value="displayValue"
:filter-function="() => rootItems"
:filter-function="filterFunction"
:class="ui.root({ class: [props.class, props.ui?.root] })"
:as-child="!!multiple"
@update:model-value="onUpdate"
@@ -412,13 +347,11 @@ defineExpose({
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? t('inputMenu.noMatch', { searchTerm }) : t('inputMenu.noData') }}
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
</slot>
</ComboboxEmpty>
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'top'" />
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
@@ -465,8 +398,6 @@ defineExpose({
</ComboboxItem>
</template>
</ComboboxGroup>
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'bottom'" />
</ComboboxViewport>
<ComboboxArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />

View File

@@ -1,192 +0,0 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { NumberFieldRootProps } from 'radix-vue'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/input-number'
import type { ButtonProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { inputNumber: Partial<typeof theme> } }
const inputNumber = tv({ extend: tv(theme), ...(appConfig.ui?.inputNumber || {}) })
type InputNumberVariants = VariantProps<typeof inputNumber>
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
class?: any
/** The placeholder text when the input is empty. */
placeholder?: string
ui?: Partial<typeof inputNumber.slots>
color?: InputNumberVariants['color']
variant?: InputNumberVariants['variant']
size?: InputNumberVariants['size']
/** Highlight the ring color like a focus state. */
highlight?: boolean
/**
* The orientation of the input menu.
* @defaultValue 'horizontal'
*/
orientation?: 'vertical' | 'horizontal'
/**
* Configure the increment button. The `color` and `size` are inherited.
* @defaultValue { variant: 'link' }
*/
increment?: ButtonProps
/**
* The icon displayed to increment the value.
* @defaultValue appConfig.ui.icons.plus
*/
incrementIcon?: string
/**
* Configure the decrement button. The `color` and `size` are inherited.
* @defaultValue { variant: 'link' }
*/
decrement?: ButtonProps
/**
* The icon displayed to decrement the value.
* @defaultValue appConfig.ui.icons.minus
*/
decrementIcon?: string
autofocus?: boolean
autofocusDelay?: number
/**
* The locale to use for formatting and parsing numbers.
* @defaultValue UApp.locale.code
*/
locale?: string
}
export interface InputNumberEmits {
(e: 'update:modelValue', payload: number): void
(e: 'blur', event: FocusEvent): void
(e: 'change', payload: Event): void
}
export interface InputNumberSlots {
increment(props?: {}): any
decrement(props?: {}): any
}
</script>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { NumberFieldRoot, NumberFieldInput, NumberFieldDecrement, NumberFieldIncrement, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import UButton from './Button.vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<InputNumberProps>(), {
orientation: 'horizontal'
})
const emits = defineEmits<InputNumberEmits>()
defineSlots<InputNumberSlots>()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'formatOptions'), emits)
const { emitFormBlur, emitFormChange, emitFormInput, id, color, size, name, highlight, disabled } = useFormField<InputNumberProps>(props)
const { t, code: codeLocale } = useLocale()
const locale = computed(() => props.locale || codeLocale.value)
const ui = computed(() => inputNumber({
color: color.value,
variant: props.variant,
size: size.value,
highlight: highlight.value,
orientation: props.orientation
}))
const incrementIcon = computed(() => props.incrementIcon || (props.orientation === 'horizontal' ? appConfig.ui.icons.plus : appConfig.ui.icons.chevronUp))
const decrementIcon = computed(() => props.decrementIcon || (props.orientation === 'horizontal' ? appConfig.ui.icons.minus : appConfig.ui.icons.chevronDown))
const inputRef = ref<InstanceType<typeof NumberFieldInput> | null>(null)
function autoFocus() {
if (props.autofocus) {
inputRef.value?.$el?.focus()
}
}
onMounted(() => {
setTimeout(() => {
autoFocus()
}, props.autofocusDelay)
})
function onUpdate(value: number) {
// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
emitFormChange()
emitFormInput()
}
function onBlur(event: FocusEvent) {
emitFormBlur()
emits('blur', event)
}
defineExpose({
inputRef
})
</script>
<template>
<NumberFieldRoot
v-bind="rootProps"
:id="id"
:class="ui.root({ class: [props.class, props.ui?.root] })"
:name="name"
:disabled="disabled"
:locale="locale"
@update:model-value="onUpdate"
>
<NumberFieldInput
v-bind="$attrs"
ref="inputRef"
:placeholder="placeholder"
:required="required"
:class="ui.base({ class: props.ui?.base })"
@blur="onBlur"
/>
<div :class="ui.increment({ class: props.ui?.increment })">
<NumberFieldIncrement as-child :disabled="disabled">
<slot name="increment">
<UButton
:icon="incrementIcon"
:color="color"
:size="size"
variant="link"
:aria-label="t('inputNumber.increment')"
v-bind="typeof increment === 'object' ? increment : undefined"
/>
</slot>
</NumberFieldIncrement>
</div>
<div :class="ui.decrement({ class: props.ui?.decrement })">
<NumberFieldDecrement as-child :disabled="disabled">
<slot name="decrement">
<UButton
:icon="decrementIcon"
:color="color"
:size="size"
variant="link"
:aria-label="t('inputNumber.decrement')"
v-bind="typeof decrement === 'object' ? decrement : undefined"
/>
</slot>
</NumberFieldDecrement>
</div>
</NumberFieldRoot>
</template>

View File

@@ -73,8 +73,8 @@ export interface LinkProps extends NuxtLinkProps {
active?: boolean
/** Will only be active if the current route is an exact match. */
exact?: boolean
/** Allows controlling how the current route query sets the link as active. */
exactQuery?: boolean | 'partial'
/** Will only be active if the current route query is an exact match. */
exactQuery?: boolean
/** Will only be active if the current route hash is an exact match. */
exactHash?: boolean
/** The class to apply when the link is inactive. */
@@ -94,11 +94,10 @@ extendDevtoolsMeta({ example: 'LinkExample' })
<script setup lang="ts">
import { computed } from 'vue'
import { isEqual, diff } from 'ohash'
import { isEqual } from 'ohash'
import { useForwardProps } from 'radix-vue'
import { reactiveOmit } from '@vueuse/core'
import { useRoute } from '#imports'
import ULinkBase from './LinkBase.vue'
defineOptions({ inheritAttrs: false })
@@ -124,27 +123,14 @@ const ui = computed(() => tv({
}
}))
function isPartiallyEqual(item1: any, item2: any) {
const diffedKeys = diff(item1, item2).reduce((filtered, q) => {
if (q.type === 'added') {
filtered.push(q.key)
}
return filtered
}, [] as string[])
return isEqual(item1, item2, { excludeKeys: key => diffedKeys.includes(key) })
}
function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
if (props.active !== undefined) {
return props.active
}
if (props.exactQuery === 'partial') {
if (!isPartiallyEqual(linkRoute.query, route.query)) return false
} else if (props.exactQuery === true) {
if (!isEqual(linkRoute.query, route.query)) return false
if (props.exactQuery && !isEqual(linkRoute.query, route.query)) {
return false
}
if (props.exactHash && linkRoute.hash !== route.hash) {
return false
}

View File

@@ -77,7 +77,6 @@ import { computed, toRef } from 'vue'
import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import UButton from './Button.vue'
const props = withDefaults(defineProps<ModalProps>(), {
@@ -96,22 +95,14 @@ const contentEvents = computed(() => {
if (props.preventClose) {
return {
pointerDownOutside: (e: Event) => e.preventDefault(),
interactOutside: (e: Event) => e.preventDefault(),
escapeKeyDown: (e: Event) => e.preventDefault()
interactOutside: (e: Event) => e.preventDefault()
}
}
return {
interactOutside: (e: Event) => {
if (e.target instanceof Element && e.target.closest('[data-sonner-toaster]')) {
return e.preventDefault()
}
}
}
return {}
})
const appConfig = useAppConfig()
const { t } = useLocale()
const ui = computed(() => modal({
transition: props.transition,
@@ -152,7 +143,7 @@ const ui = computed(() => modal({
size="md"
color="neutral"
variant="ghost"
:aria-label="t('modal.close')"
aria-label="Close"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
/>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, CollapsibleRootProps } from 'radix-vue'
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps } from 'radix-vue'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/navigation-menu'
@@ -17,7 +17,7 @@ export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'child
description?: string
}
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'> {
label?: string
icon?: string
avatar?: AvatarProps
@@ -208,8 +208,6 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
:key="`list-${listIndex}-${index}`"
as="li"
:value="item.value || String(index)"
:default-open="item.defaultOpen"
:open="item.open"
:class="ui.item({ class: props.ui?.item })"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>

View File

@@ -103,11 +103,9 @@ extendDevtoolsMeta({ defaultProps: { total: 50 } })
</script>
<script setup lang="ts">
import { computed } from 'vue'
import { PaginationRoot, PaginationList, PaginationListItem, PaginationFirst, PaginationPrev, PaginationEllipsis, PaginationNext, PaginationLast, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import UButton from './Button.vue'
const props = withDefaults(defineProps<PaginationProps>(), {
@@ -126,13 +124,8 @@ const emits = defineEmits<PaginationEmits>()
const slots = defineSlots<PaginationSlots>()
const appConfig = useAppConfig()
const { dir } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultPage', 'disabled', 'itemsPerPage', 'page', 'showEdges', 'siblingCount', 'total'), emits)
const firstIcon = computed(() => props.firstIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronDoubleRight : appConfig.ui.icons.chevronDoubleLeft))
const prevIcon = computed(() => props.prevIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronRight : appConfig.ui.icons.chevronLeft))
const nextIcon = computed(() => props.nextIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronLeft : appConfig.ui.icons.chevronRight))
const lastIcon = computed(() => props.lastIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronDoubleLeft : appConfig.ui.icons.chevronDoubleRight))
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultPage', 'disabled', 'itemsPerPage', 'page', 'showEdges', 'siblingCount', 'total'), emits)
// eslint-disable-next-line vue/no-dupe-keys
const ui = pagination()
@@ -143,12 +136,12 @@ const ui = pagination()
<PaginationList v-slot="{ items }" :class="ui.list({ class: props.ui?.list })">
<PaginationFirst v-if="showControls || !!slots.first" as-child>
<slot name="first">
<UButton :color="color" :variant="variant" :size="size" :icon="firstIcon" :to="to?.(1)" />
<UButton :color="color" :variant="variant" :size="size" :icon="firstIcon || appConfig.ui.icons.chevronDoubleLeft" :to="to?.(1)" />
</slot>
</PaginationFirst>
<PaginationPrev v-if="showControls || !!slots.prev" as-child>
<slot name="prev">
<UButton :color="color" :variant="variant" :size="size" :icon="prevIcon" :to="page > 1 ? to?.(page - 1) : undefined" />
<UButton :color="color" :variant="variant" :size="size" :icon="prevIcon || appConfig.ui.icons.chevronLeft" :to="page > 1 ? to?.(page - 1) : undefined" />
</slot>
</PaginationPrev>
@@ -176,12 +169,12 @@ const ui = pagination()
<PaginationNext v-if="showControls || !!slots.next" as-child>
<slot name="next">
<UButton :color="color" :variant="variant" :size="size" :icon="nextIcon" :to="page < pageCount ? to?.(pageCount) : undefined" />
<UButton :color="color" :variant="variant" :size="size" :icon="nextIcon || appConfig.ui.icons.chevronRight" :to="page < pageCount ? to?.(pageCount) : undefined" />
</slot>
</PaginationNext>
<PaginationLast v-if="showControls || !!slots.last" as-child>
<slot name="last">
<UButton :color="color" :variant="variant" :size="size" :icon="lastIcon" :to=" to?.(pageCount)" />
<UButton :color="color" :variant="variant" :size="size" :icon="lastIcon || appConfig.ui.icons.chevronDoubleRight" :to=" to?.(pageCount)" />
</slot>
</PaginationLast>
</PaginationList>

View File

@@ -1,96 +0,0 @@
<script lang="ts">
import _appConfig from '#build/app.config'
import theme from '#build/ui/pin-input'
import type { AppConfig } from '@nuxt/schema'
import type { PinInputRootEmits, PinInputRootProps } from 'radix-vue'
import { tv, type VariantProps } from 'tailwind-variants'
import type { PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { pinInput: Partial<typeof theme> } }
const pinInput = tv({ extend: tv(theme), ...(appConfig.ui?.pinInput || {}) })
type PinInputVariants = VariantProps<typeof pinInput>
export interface PinInputProps extends Pick<PinInputRootProps, 'defaultValue' | 'disabled' | 'id' | 'mask' | 'modelValue' | 'name' | 'otp' | 'placeholder' | 'required' | 'type'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
color?: PinInputVariants['color']
variant?: PinInputVariants['variant']
size?: PinInputVariants['size']
length?: number | string
highlight?: boolean
class?: any
ui?: PartialString<typeof pinInput.slots>
}
export type PinInputEmits = PinInputRootEmits & {
change: [payload: Event]
blur: [payload: Event]
}
</script>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { PinInputInput, PinInputRoot, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { useFormField } from '../composables/useFormField'
import { looseToNumber } from '../utils'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<PinInputProps>(), {
type: 'text',
length: 5
})
const emits = defineEmits<PinInputEmits>()
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'placeholder', 'required', 'type'), emits)
const { emitFormInput, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled } = useFormField<PinInputProps>(props)
const ui = computed(() => pinInput({
color: color.value,
variant: props.variant,
size: size.value,
highlight: highlight.value
}))
const completed = ref(false)
function onComplete(value: string[]) {
// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
emitFormChange()
}
function onBlur(event: FocusEvent) {
if (!event.relatedTarget || completed.value) {
emits('blur', event)
emitFormBlur()
}
}
</script>
<template>
<PinInputRoot
v-bind="rootProps"
:id="id"
:name="name"
:class="ui.root({ class: [props.class, props.ui?.root] })"
@update:model-value="emitFormInput()"
@complete="onComplete"
>
<PinInputInput
v-for="(ids, index) in looseToNumber(props.length)"
:key="ids"
:index="index"
:class="ui.base({ class: props.ui?.base })"
v-bind="$attrs"
:disabled="disabled"
@blur="onBlur"
/>
</PinInputRoot>
</template>

View File

@@ -31,11 +31,6 @@ export interface PopoverProps extends PopoverRootProps, Pick<HoverCardRootProps,
* @defaultValue true
*/
portal?: boolean
/**
* When `true`, the popover will not close when clicking outside.
* @defaultValue false
*/
preventClose?: boolean
class?: any
ui?: Partial<typeof popover.slots>
}
@@ -69,17 +64,6 @@ const slots = defineSlots<PopoverSlots>()
const pick = props.mode === 'hover' ? reactivePick(props, 'defaultOpen', 'open', 'openDelay', 'closeDelay') : reactivePick(props, 'defaultOpen', 'open', 'modal')
const rootProps = useForwardPropsEmits(pick, emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8 }) as PopoverContentProps)
const contentEvents = computed(() => {
if (props.preventClose) {
return {
pointerDownOutside: (e: Event) => e.preventDefault(),
interactOutside: (e: Event) => e.preventDefault(),
escapeKeyDown: (e: Event) => e.preventDefault()
}
}
return {}
})
const arrowProps = toRef(() => props.arrow as PopoverArrowProps)
// eslint-disable-next-line vue/no-dupe-keys
@@ -97,7 +81,7 @@ const Component = computed(() => props.mode === 'hover' ? HoverCard : Popover)
</Component.Trigger>
<Component.Portal :disabled="!portal">
<Component.Content v-bind="contentProps" :class="ui.content({ class: [props.class, props.ui?.content] })" v-on="contentEvents">
<Component.Content v-bind="contentProps" :class="ui.content({ class: [props.class, props.ui?.content] })">
<slot name="content" />
<Component.Arrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />

View File

@@ -34,12 +34,11 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
/** The placeholder text when the select is empty. */
placeholder?: string
/**
* Whether to display the search input or not.
* Wether to display the search input or not.
* Can be an object to pass additional props to the input.
* `{ placeholder: 'Search...', variant: 'none' }`{lang="ts-type"}
* @defaultValue true
* @defaultValue { placeholder: 'Search...' }
*/
searchInput?: boolean | InputProps
searchInput?: boolean | { placeholder?: string }
color?: SelectMenuVariants['color']
variant?: SelectMenuVariants['variant']
size?: SelectMenuVariants['size']
@@ -70,10 +69,9 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
*/
portal?: boolean
/**
* Whether to filter items or not, can be an array of fields to filter. Defaults to `[labelKey]`.
* Whether to filter items or not, can be an array of fields to filter.
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
* `['label']`{lang="ts-type"}
* @defaultValue true
* @defaultValue ['label']
*/
filter?: boolean | string[]
/**
@@ -89,11 +87,6 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
items?: I
/** Highlight the ring color like a focus state. */
highlight?: boolean
/**
* Determines if custom user input that does not exist in options can be added.
* @defaultValue false
*/
createItem?: boolean | 'always' | { placement?: 'top' | 'bottom', when?: 'empty' | 'always' }
class?: any
ui?: PartialString<typeof selectMenu.slots>
/** The controlled value of the Combobox. Can be binded-with with `v-model`. */
@@ -106,7 +99,6 @@ export type SelectMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>
change: [payload: Event]
blur: [payload: FocusEvent]
focus: [payload: FocusEvent]
create: [payload: Event, item: T]
} & SelectModelValueEmits<T, V, M>
type SlotProps<T> = (props: { item: T, index: number }) => any
@@ -120,35 +112,32 @@ export interface SelectMenuSlots<T> {
'item-leading': SlotProps<T>
'item-label': SlotProps<T>
'item-trailing': SlotProps<T>
'create-item-label'(props: { item: T }): any
}
extendDevtoolsMeta({ defaultProps: { items: ['Option 1', 'Option 2', 'Option 3'] } })
</script>
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
import { computed, toRef, toRaw } from 'vue'
import { computed, toRef } from 'vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits } from 'radix-vue'
import { defu } from 'defu'
import { isEqual } from 'ohash'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import { get, escapeRegExp } from '../utils'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import UChip from './Chip.vue'
import UInput from './Input.vue'
const props = withDefaults(defineProps<SelectMenuProps<T, I, V, M>>(), {
search: true,
portal: true,
autofocusDelay: 0,
searchInput: true,
filter: true,
searchInput: () => ({ placeholder: 'Search...' }),
filter: () => ['label'],
labelKey: 'label' as never
})
@@ -158,17 +147,12 @@ const slots = defineSlots<SelectMenuSlots<T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'resetSearchTermOnBlur'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: 'Search...', variant: 'none' }) as InputProps)
// This is a hack due to generic boolean casting (see https://github.com/nuxt/ui/issues/2541)
const multiple = toRef(() => typeof props.multiple === 'string' ? true : props.multiple)
const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate()
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled } = useFormField<InputProps>(props)
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
@@ -195,32 +179,28 @@ function displayValue(value: T | T[]): string {
return value && (typeof value === 'object' ? get(value, props.labelKey as string) : value)
}
const item = items.value.find(item => isEqual(get(item as Record<string, any>, props.valueKey as string), value)) ?? (props.createItem && value)
const item = items.value.find(item => isEqual(get(item as Record<string, any>, props.valueKey as string), value))
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
}
function filterFunction(
inputItems: ArrayOrWrapped<T> = items.value as ArrayOrWrapped<T>,
filterSearchTerm: string = searchTerm.value,
comparator = (item: any, term: string) => String(item).search(new RegExp(term, 'i')) !== -1
): ArrayOrWrapped<T> {
function filterFunction(items: ArrayOrWrapped<T>, searchTerm: string): ArrayOrWrapped<T> {
if (props.filter === false) {
return inputItems
return items
}
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
const escapedSearchTerm = escapeRegExp(filterSearchTerm)
const escapedSearchTerm = escapeRegExp(searchTerm)
return inputItems.filter((item: T) => {
return items.filter((item: T) => {
if (typeof item !== 'object') {
return comparator(item, escapedSearchTerm)
return String(item).search(new RegExp(escapedSearchTerm, 'i')) !== -1
}
return fields.some((field) => {
const child = get(item, field as string)
return child !== null && child !== undefined && comparator(child, escapedSearchTerm)
return child !== null && child !== undefined && String(child).search(new RegExp(escapedSearchTerm, 'i')) !== -1
})
}) as ArrayOrWrapped<T>
}
@@ -229,40 +209,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
// eslint-disable-next-line vue/no-dupe-keys
const items = computed(() => groups.value.flatMap(group => group) as T[])
const creatable = computed(() => {
if (!props.createItem) {
return false
}
const isModelValueCustom = props.modelValue && filterFunction((props.multiple && Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue]) as ArrayOrWrapped<T>, searchTerm.value, (item, term) => String(item) === term).length === 1
if (isModelValueCustom) {
return false
}
const filteredItems = filterFunction()
const newItem = searchTerm.value && {
item: props.valueKey ? { [props.valueKey]: searchTerm.value, [props.labelKey ?? 'label']: searchTerm.value } : searchTerm.value,
position: ((typeof props.createItem === 'object' && props.createItem.placement) || 'bottom') as 'top' | 'bottom'
}
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
return (filteredItems.length === 1 && filterFunction(filteredItems, searchTerm.value, (item, term) => String(item) === term).length === 1) ? false : newItem
}
return filteredItems.length > 0 ? false : newItem
})
const rootItems = computed(() => [
...(creatable.value && creatable.value.position === 'top' ? [creatable.value.item] : []),
...filterFunction(),
...(creatable.value && creatable.value.position === 'bottom' ? [creatable.value.item] : [])
] as ArrayOrWrapped<T>)
function onUpdate(value: any) {
if (toRaw(props.modelValue) === value) {
return
}
// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
@@ -283,22 +230,6 @@ function onUpdateOpen(value: boolean) {
</script>
<template>
<DefineCreateItemTemplate>
<ComboboxGroup v-if="creatable" :class="ui.group({ class: props.ui?.group })">
<ComboboxItem
:class="ui.item({ class: props.ui?.item })"
:value="valueKey && typeof creatable.item === 'object' ? get(creatable.item, props.valueKey as string) : creatable.item"
@select="e => emits('create', e, (creatable as any).item as T)"
>
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
<slot name="create-item-label" :item="(creatable.item as T)">
{{ t('selectMenu.create', { label: typeof creatable.item === 'object' ? get(creatable.item, props.labelKey as string) : creatable.item }) }}
</slot>
</span>
</ComboboxItem>
</ComboboxGroup>
</DefineCreateItemTemplate>
<ComboboxRoot
:id="id"
v-slot="{ modelValue, open }"
@@ -309,7 +240,7 @@ function onUpdateOpen(value: boolean) {
:disabled="disabled"
:multiple="multiple"
:display-value="() => searchTerm"
:filter-function="() => rootItems"
:filter-function="filterFunction"
@update:model-value="onUpdate"
@update:open="onUpdateOpen"
>
@@ -343,19 +274,21 @@ function onUpdateOpen(value: boolean) {
<ComboboxPortal :disabled="!portal">
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
<ComboboxInput v-if="!!searchInput" as-child>
<UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
</ComboboxInput>
<ComboboxInput
v-if="!!searchInput"
autofocus
autocomplete="off"
v-bind="typeof searchInput === 'object' ? searchInput : {}"
:class="ui.input({ class: props.ui?.input })"
/>
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? t('selectMenu.noMatch', { searchTerm }) : t('selectMenu.noData') }}
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
</slot>
</ComboboxEmpty>
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'top'" />
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
@@ -402,8 +335,6 @@ function onUpdateOpen(value: boolean) {
</ComboboxItem>
</template>
</ComboboxGroup>
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'bottom'" />
</ComboboxViewport>
<ComboboxArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />

View File

@@ -75,7 +75,6 @@ import { computed, toRef } from 'vue'
import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import UButton from './Button.vue'
const props = withDefaults(defineProps<SlideoverProps>(), {
@@ -95,22 +94,14 @@ const contentEvents = computed(() => {
if (props.preventClose) {
return {
pointerDownOutside: (e: Event) => e.preventDefault(),
interactOutside: (e: Event) => e.preventDefault(),
escapeKeyDown: (e: Event) => e.preventDefault()
interactOutside: (e: Event) => e.preventDefault()
}
}
return {
interactOutside: (e: Event) => {
if (e.target instanceof Element && e.target.closest('[data-sonner-toaster]')) {
return e.preventDefault()
}
}
}
return {}
})
const appConfig = useAppConfig()
const { t } = useLocale()
const ui = computed(() => slideover({
transition: props.transition,
@@ -151,7 +142,7 @@ const ui = computed(() => slideover({
size="md"
color="neutral"
variant="ghost"
:aria-label="t('slideover.close')"
aria-label="Close"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
/>

View File

@@ -41,7 +41,6 @@ export interface TableData {
export interface TableProps<T> {
data?: T[]
columns?: TableColumn<T>[]
caption?: string
/**
* Whether the table should have a sticky header.
* @defaultValue false
@@ -96,7 +95,6 @@ type DynamicCellSlots<T, K = keyof T> = Record<string, (props: CellContext<T, un
export type TableSlots<T> = {
expanded: (props: { row: Row<T> }) => any
empty: (props?: {}) => any
caption: (props?: {}) => any
} & DynamicHeaderSlots<T> & DynamicCellSlots<T>
</script>
@@ -112,12 +110,10 @@ import {
useVueTable
} from '@tanstack/vue-table'
import { upperFirst } from 'scule'
import { useLocale } from '../composables/useLocale'
const props = defineProps<TableProps<T>>()
defineSlots<TableSlots<T>>()
const { t } = useLocale()
const data = computed(() => props.data ?? [])
const columns = computed<TableColumn<T>[]>(() => props.columns ?? Object.keys(data.value[0] ?? {}).map((accessorKey: string) => ({ accessorKey, header: upperFirst(accessorKey) })))
@@ -194,12 +190,6 @@ defineExpose({
<template>
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
<table :class="ui.base({ class: [props.ui?.base] })">
<caption v-if="caption" :class="ui.caption({ class: [props.ui?.caption] })">
<slot name="caption">
{{ caption }}
</slot>
</caption>
<thead :class="ui.thead({ class: [props.ui?.thead] })">
<tr v-for="headerGroup in tableApi.getHeaderGroups()" :key="headerGroup.id" :class="ui.tr({ class: [props.ui?.tr] })">
<th
@@ -241,7 +231,7 @@ defineExpose({
<tr v-else :class="ui.tr({ class: [props.ui?.tr] })">
<td :colspan="columns?.length" :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty">
{{ t('table.noData') }}
No results
</slot>
</td>
</tr>

View File

@@ -151,7 +151,7 @@ function autoResize() {
}
}
watch(modelValue, () => {
watch(() => modelValue, () => {
nextTick(autoResize)
})

View File

@@ -28,7 +28,6 @@ export interface ToastProps extends Pick<ToastRootProps, 'defaultOpen' | 'open'
* Display a list of actions:
* - under the title and description if multiline
* - next to the close button if not multiline
* `{ size: 'xs' }`{lang="ts-type"}
*/
actions?: ButtonProps[]
/**
@@ -64,7 +63,6 @@ import { ref, computed, onMounted } from 'vue'
import { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import UButton from './Button.vue'
@@ -76,7 +74,6 @@ const emits = defineEmits<ToastEmits>()
const slots = defineSlots<ToastSlots>()
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'duration', 'type'), emits)
const multiline = computed(() => !!props.title && !!props.description)
@@ -154,7 +151,7 @@ defineExpose({
size="md"
color="neutral"
variant="link"
:aria-label="t('toast.close')"
aria-label="Close"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
@click.stop

View File

@@ -19,11 +19,6 @@ export interface ToasterProps extends Omit<ToastProviderProps, 'swipeDirection'>
* @defaultValue true
*/
expand?: boolean
/**
* Render the toaster in a portal.
* @defaultValue true
*/
portal?: boolean
class?: any
ui?: Partial<typeof toaster.slots>
}
@@ -41,7 +36,7 @@ extendDevtoolsMeta({ example: 'ToasterExample' })
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ToastProvider, ToastViewport, ToastPortal, useForwardProps } from 'radix-vue'
import { ToastProvider, ToastViewport, useForwardProps } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { useToast } from '../composables/useToast'
import { omit } from '../utils'
@@ -49,7 +44,6 @@ import UToast from './Toast.vue'
const props = withDefaults(defineProps<ToasterProps>(), {
expand: true,
portal: true,
duration: 5000
})
defineSlots<ToasterSlots>()
@@ -126,20 +120,18 @@ function getOffset(index: number) {
@click="toast.click && toast.click(toast)"
/>
<ToastPortal :disabled="!portal">
<ToastViewport
:data-expanded="expanded"
:class="ui.viewport({ class: [props.class, props.ui?.viewport] })"
:style="{
'--scale-factor': '0.05',
'--translate-factor': position?.startsWith('top') ? '1px' : '-1px',
'--gap': position?.startsWith('top') ? '16px' : '-16px',
'--front-height': `${frontHeight}px`,
'--height': `${height}px`
}"
@mouseenter="hovered = true"
@mouseleave="hovered = false"
/>
</ToastPortal>
<ToastViewport
:data-expanded="expanded"
:class="ui.viewport({ class: [props.class, props.ui?.viewport] })"
:style="{
'--scale-factor': '0.05',
'--translate-factor': position?.startsWith('top') ? '1px' : '-1px',
'--gap': position?.startsWith('top') ? '16px' : '-16px',
'--front-height': `${frontHeight}px`,
'--height': `${height}px`
}"
@mouseenter="hovered = true"
@mouseleave="hovered = false"
/>
</ToastProvider>
</template>

View File

@@ -1,13 +0,0 @@
import { defu } from 'defu'
import type { Locale, Direction, Messages } from '../types/locale'
interface DefineLocaleOptions {
name: string
code: string
dir?: Direction
messages: Messages
}
export function defineLocale(options: DefineLocaleOptions): Locale {
return defu<DefineLocaleOptions, [{ dir: Direction }]>(options, { dir: 'ltr' })
}

View File

@@ -19,7 +19,7 @@ export const formOptionsInjectionKey: InjectionKey<ComputedRef<FormInjectedOptio
export const formBusInjectionKey: InjectionKey<UseEventBusReturn<FormEvent, string>> = Symbol('nuxt-ui.form-events')
export const formFieldInjectionKey: InjectionKey<ComputedRef<FormFieldInjectedOptions<FormFieldProps>>> = Symbol('nuxt-ui.form-field')
export const inputIdInjectionKey: InjectionKey<Ref<string | undefined>> = Symbol('nuxt-ui.input-id')
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, { id?: string, pattern?: RegExp }>>> = Symbol('nuxt-ui.form-inputs')
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, string>>> = Symbol('nuxt-ui.form-inputs')
export const formLoadingInjectionKey: InjectionKey<Readonly<Ref<boolean>>> = Symbol('nuxt-ui.form-loading')
export function useFormField<T>(props?: Props<T>, opts?: { bind?: boolean }) {
@@ -38,7 +38,7 @@ export function useFormField<T>(props?: Props<T>, opts?: { bind?: boolean }) {
inputId.value = props?.id
}
if (formInputs && formField.value.name && inputId.value) {
formInputs.value[formField.value.name] = { id: inputId.value, pattern: formField.value.errorPattern }
formInputs.value[formField.value.name] = inputId.value
}
}

Some files were not shown because too many files have changed in this diff Show More