mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 12:39:35 +01:00
Compare commits
89 Commits
fix/form-e
...
v3.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21d8c352a9 | ||
|
|
5deadc7096 | ||
|
|
fa9f0a7e2a | ||
|
|
143c015bbd | ||
|
|
d75f47419d | ||
|
|
7b148daf1f | ||
|
|
30e0c7fddd | ||
|
|
14fb21be00 | ||
|
|
25091bad48 | ||
|
|
b75ed29068 | ||
|
|
b2fa65734b | ||
|
|
d3a079a644 | ||
|
|
8c6a8c283f | ||
|
|
2fc36c878c | ||
|
|
992be91823 | ||
|
|
bd2f077fe8 | ||
|
|
7329900ae5 | ||
|
|
8d85498ee1 | ||
|
|
5c292cf620 | ||
|
|
c0837059a9 | ||
|
|
f5ea2411dc | ||
|
|
1fbbfe8df0 | ||
|
|
0daac5bafb | ||
|
|
756f791a1a | ||
|
|
8ed434c105 | ||
|
|
190a2c9799 | ||
|
|
e55c0e2594 | ||
|
|
4312ca4702 | ||
|
|
2289742656 | ||
|
|
601f4b2cd2 | ||
|
|
cd080541a0 | ||
|
|
5722e0802d | ||
|
|
8d0026558a | ||
|
|
e5119a9ca4 | ||
|
|
976dd2a386 | ||
|
|
1d95eb7246 | ||
|
|
7cc26d098d | ||
|
|
9241ba1230 | ||
|
|
3396d5cebe | ||
|
|
937585cb3f | ||
|
|
9c00f7c7b7 | ||
|
|
73e25ed235 | ||
|
|
75c5e87724 | ||
|
|
95aa6f68b3 | ||
|
|
f516d7b36d | ||
|
|
300ccc4885 | ||
|
|
e48b416e3b | ||
|
|
17170bb998 | ||
|
|
fa5a3752c9 | ||
|
|
fc9711223b | ||
|
|
8a8b1ee2e1 | ||
|
|
30218f1b5b | ||
|
|
3584a3328b | ||
|
|
6d3dbdbee5 | ||
|
|
c614a0aafc | ||
|
|
df7a61a97a | ||
|
|
143612ec73 | ||
|
|
18931acdb3 | ||
|
|
bbc6bf2455 | ||
|
|
ff1e0798d3 | ||
|
|
b0be26d67f | ||
|
|
36ea3e4045 | ||
|
|
4889d30b44 | ||
|
|
944a7e0f07 | ||
|
|
d6943e39c0 | ||
|
|
ddb46905e7 | ||
|
|
0e74dbebce | ||
|
|
9e2cc5b125 | ||
|
|
ea97759c2c | ||
|
|
95a0bbc581 | ||
|
|
ecd63ad8d6 | ||
|
|
47f58f52ef | ||
|
|
446f9c1085 | ||
|
|
7e8a1dd496 | ||
|
|
89ee31b7ae | ||
|
|
95be76940c | ||
|
|
761afaf40d | ||
|
|
d167c9b807 | ||
|
|
824ba56291 | ||
|
|
4fbbb25f68 | ||
|
|
602a667343 | ||
|
|
febda5c2b6 | ||
|
|
20379f51cc | ||
|
|
1ec56f3326 | ||
|
|
1f44d58b64 | ||
|
|
5392f988b8 | ||
|
|
26362408b1 | ||
|
|
1e7638bd03 | ||
|
|
afe40033b0 |
15
.github/ISSUE_TEMPLATE/bug-v3.yml
vendored
15
.github/ISSUE_TEMPLATE/bug-v3.yml
vendored
@@ -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,11 +29,20 @@ 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.5
|
||||
placeholder: v3.0.0-alpha.x
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -12,7 +12,7 @@ body:
|
||||
label: For what version of Nuxt UI are you suggesting this?
|
||||
options:
|
||||
- v2.x
|
||||
- v3-alpha
|
||||
- v3.0.0-alpha.x
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/question.yml
vendored
2
.github/ISSUE_TEMPLATE/question.yml
vendored
@@ -12,7 +12,7 @@ body:
|
||||
label: For what version of Nuxt UI are you asking this question?
|
||||
options:
|
||||
- v2.x
|
||||
- v3-alpha
|
||||
- v3.0.0-alpha.x
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
3
.github/workflows/ci-v3.yml
vendored
3
.github/workflows/ci-v3.yml
vendored
@@ -61,5 +61,8 @@ 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
|
||||
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -1,5 +1,67 @@
|
||||
# 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
|
||||
|
||||
64
README.md
64
README.md
@@ -1,6 +1,6 @@
|
||||
[](https://ui.nuxt.com)
|
||||
|
||||
# Nuxt UI v3
|
||||
# Nuxt UI
|
||||
|
||||
[![npm version][npm-version-src]][npm-version-href]
|
||||
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
||||
@@ -9,9 +9,14 @@
|
||||
|
||||
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.
|
||||
|
||||
## Installation
|
||||
> [!NOTE]
|
||||
> You are on the `v3` development branch, check out the [dev branch](https://github.com/nuxt/ui) for Nuxt UI v2.
|
||||
|
||||
1. Install the Nuxt UI v3 alpha package:
|
||||
## Documentation
|
||||
|
||||
Visit https://ui3.nuxt.dev to explore the documentation.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add @nuxt/ui@next
|
||||
@@ -29,10 +34,9 @@ npm install @nuxt/ui@next
|
||||
bun add @nuxt/ui@next
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> Make sure you have `typescript` installed in your dev dependencies.
|
||||
### Nuxt
|
||||
|
||||
2. Register the Nuxt UI module in your `nuxt.config.ts`:
|
||||
1. Add the Nuxt UI module in your `nuxt.config.ts`:
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
@@ -40,18 +44,54 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
3. Import Tailwind CSS and Nuxt UI in your `app.vue` or [CSS](https://nuxt.com/docs/getting-started/styling#the-css-property):
|
||||
2. Import Tailwind CSS and Nuxt UI in your CSS:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
</style>
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/installation/nuxt).
|
||||
|
||||
Visit https://ui3.nuxt.dev to explore the 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).
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
@@ -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: 'init',
|
||||
description: 'Init a new component.'
|
||||
name: 'component',
|
||||
description: 'Make a new component.'
|
||||
},
|
||||
args: {
|
||||
name: {
|
||||
14
cli/commands/make/index.mjs
Normal file
14
cli/commands/make/index.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
64
cli/commands/make/locale.mjs
Normal file
64
cli/commands/make/locale.mjs
Normal file
@@ -0,0 +1,64 @@
|
||||
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}`)
|
||||
}
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { defineCommand, runMain } from 'citty'
|
||||
import init from './commands/init.mjs'
|
||||
import make from './commands/make/index.mjs'
|
||||
|
||||
const main = defineCommand({
|
||||
meta: {
|
||||
@@ -8,7 +8,7 @@ const main = defineCommand({
|
||||
description: 'Nuxt UI CLI'
|
||||
},
|
||||
subCommands: {
|
||||
init
|
||||
make
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -163,9 +163,54 @@ 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
|
||||
test,
|
||||
doc
|
||||
}
|
||||
|
||||
@@ -15,3 +15,17 @@ 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()
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ const isDark = computed({
|
||||
@import '@nuxt/ui';
|
||||
|
||||
@theme {
|
||||
--font-family-sans: 'DM Sans', sans-serif;
|
||||
--font-sans: 'DM Sans', sans-serif;
|
||||
|
||||
--color-primary-50: var(--ui-color-primary-50);
|
||||
--color-primary-100: var(--ui-color-primary-100);
|
||||
|
||||
@@ -18,7 +18,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border rounded border-[var(--ui-border)]">
|
||||
<div class="border rounded-[var(--ui-radius)] border-[var(--ui-border)]">
|
||||
<div
|
||||
ref="wrapper"
|
||||
:class="['overflow-hidden', collapsed && overflow ? 'max-h-48' : 'max-h-none']"
|
||||
|
||||
@@ -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-lg 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-[calc(var(--ui-radius)*1.5)] border border-[var(--ui-border)] bg-neutral-50 dark:bg-neutral-800" v-html="highlightedCode" />
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="link"
|
||||
|
||||
@@ -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">$1</code>')
|
||||
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>')
|
||||
})
|
||||
</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 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-[var(--ui-radius)] bg-[var(--ui-bg-elevated)]">
|
||||
{{ meta?.name }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,9 @@ const appConfig = useAppConfig()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('content'))
|
||||
const { data: files } = await useAsyncData('files', () => queryCollectionSearchSections('content', { ignoredTags: ['style'] }))
|
||||
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('content'), {
|
||||
server: false
|
||||
})
|
||||
|
||||
const searchTerm = ref('')
|
||||
|
||||
@@ -79,6 +81,11 @@ 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: []
|
||||
})
|
||||
})) || []
|
||||
})))
|
||||
@@ -117,7 +124,9 @@ provide('navigation', updatedNavigation)
|
||||
@source "../content/**/*.md";
|
||||
|
||||
@theme {
|
||||
--font-family-sans: 'Public Sans', sans-serif;
|
||||
--container-8xl: 90rem;
|
||||
|
||||
--font-sans: 'Public Sans', sans-serif;
|
||||
|
||||
--color-green-50: #EFFDF5;
|
||||
--color-green-100: #D9FBE8;
|
||||
@@ -133,6 +142,6 @@ provide('navigation', updatedNavigation)
|
||||
}
|
||||
|
||||
:root {
|
||||
--ui-container-width: 90rem;
|
||||
--ui-container: var(--container-8xl);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -164,7 +164,7 @@ const code = computed(() => {
|
||||
continue
|
||||
}
|
||||
|
||||
code += ` ${prop?.type.includes('number') ? ':' : ''}${name}="${value}"`
|
||||
code += ` ${typeof value === '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-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">
|
||||
<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">
|
||||
<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-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]">
|
||||
<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]">
|
||||
<component :is="component" v-bind="{ ...componentProps, ...componentEvents }">
|
||||
<template v-for="slot in Object.keys(slots || {})" :key="slot" #[slot]>
|
||||
<MDCSlot :name="slot" unwrap="p">
|
||||
|
||||
@@ -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-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)]">
|
||||
<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)]">
|
||||
<slot name="options" />
|
||||
|
||||
<UFormField
|
||||
|
||||
64
docs/app/components/content/SupportedLanguages.vue
Normal file
64
docs/app/components/content/SupportedLanguages.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<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>
|
||||
@@ -4,6 +4,7 @@ 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'
|
||||
}),
|
||||
@@ -29,10 +30,11 @@ 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' })
|
||||
slider: z.number().max(20, { message: 'Must be less than 20' }),
|
||||
pin: z.string().regex(/^\d$/).array().length(5)
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
type Schema = z.input<typeof schema>
|
||||
|
||||
const state = reactive<Partial<Schema>>({})
|
||||
|
||||
@@ -52,10 +54,10 @@ async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm ref="form" :state="state" :schema="schema" @submit="onSubmit">
|
||||
<UForm ref="form" :state="state" :schema="schema" class="w-full" @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-40" />
|
||||
<UInput v-model="state.input" placeholder="john@lennon.com" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
@@ -73,42 +75,48 @@ async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="select" label="Select">
|
||||
<USelect v-model="state.select" :items="items" />
|
||||
<USelect v-model="state.select" :items="items" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="selectMenu" label="Select Menu">
|
||||
<USelectMenu v-model="state.selectMenu" :items="items" />
|
||||
<USelectMenu v-model="state.selectMenu" :items="items" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="selectMenuMultiple" label="Select Menu (Multiple)">
|
||||
<USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" />
|
||||
<USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="inputMenu" label="Input Menu">
|
||||
<UInputMenu v-model="state.inputMenu" :items="items" />
|
||||
<UInputMenu v-model="state.inputMenu" :items="items" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="inputMenuMultiple" label="Input Menu (Multiple)">
|
||||
<UInputMenu v-model="state.inputMenuMultiple" multiple :items="items" />
|
||||
<UInputMenu v-model="state.inputMenuMultiple" multiple :items="items" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<span />
|
||||
<UFormField name="inputNumber" label="Input Number">
|
||||
<UInputNumber v-model="state.inputNumber" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Textarea" name="textarea">
|
||||
<UTextarea v-model="state.textarea" />
|
||||
<UTextarea v-model="state.textarea" class="w-full" />
|
||||
</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 color="neutral" type="submit">
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
|
||||
<UButton color="neutral" variant="outline" @click="form?.clear()">
|
||||
<UButton variant="outline" @click="form?.clear()">
|
||||
Clear
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<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>
|
||||
@@ -16,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<UInputMenu
|
||||
:items="users || []"
|
||||
:loading="status === 'pending'"
|
||||
:filter="['name', 'email']"
|
||||
:filter="['label', 'email']"
|
||||
icon="i-lucide-user"
|
||||
placeholder="Select user"
|
||||
class="w-80"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
const value = ref(5)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputNumber
|
||||
v-model="value"
|
||||
:format-options="{
|
||||
signDisplay: 'exceptZero',
|
||||
minimumFractionDigits: 1
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<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>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
const value = ref(0.05)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputNumber
|
||||
v-model="value"
|
||||
:step="0.01"
|
||||
:format-options="{
|
||||
style: 'percent'
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
@@ -6,7 +6,7 @@ const value = ref('Click to clear')
|
||||
<UInput
|
||||
v-model="value"
|
||||
placeholder="Type something..."
|
||||
:ui="{ trailing: 'pr-0.5' }"
|
||||
:ui="{ trailing: 'pe-1' }"
|
||||
>
|
||||
<template v-if="value?.length" #trailing>
|
||||
<UButton
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<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>
|
||||
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
@@ -40,7 +40,7 @@ const text = computed(() => {
|
||||
placeholder="Password"
|
||||
:color="color"
|
||||
:type="show ? 'text' : 'password'"
|
||||
:ui="{ trailing: 'pr-0.5' }"
|
||||
:ui="{ trailing: 'pe-1' }"
|
||||
:aria-invalid="score < 4"
|
||||
aria-describedby="password-strength"
|
||||
class="w-full"
|
||||
|
||||
@@ -8,7 +8,7 @@ const password = ref('password')
|
||||
v-model="password"
|
||||
placeholder="Password"
|
||||
:type="show ? 'text' : 'password'"
|
||||
:ui="{ trailing: 'pr-0.5' }"
|
||||
:ui="{ trailing: 'pe-1' }"
|
||||
>
|
||||
<template #trailing>
|
||||
<UButton
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<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>
|
||||
@@ -16,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<USelectMenu
|
||||
:items="users || []"
|
||||
:loading="status === 'pending'"
|
||||
:filter="['name', 'email']"
|
||||
:filter="['label', 'email']"
|
||||
icon="i-lucide-user"
|
||||
placeholder="Select user"
|
||||
class="w-80"
|
||||
|
||||
@@ -7,9 +7,9 @@ const appConfig = useAppConfig()
|
||||
<UFormField
|
||||
label="toaster.duration"
|
||||
size="sm"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
|
||||
:ui="{
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
|
||||
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
|
||||
container: 'mt-0'
|
||||
}"
|
||||
@@ -18,8 +18,7 @@ const appConfig = useAppConfig()
|
||||
v-model="appConfig.toaster.duration"
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
class="rounded rounded-l-none min-w-12"
|
||||
:search-input="false"
|
||||
:ui="{ base: 'rounded-[var(--ui-radius)] rounded-l-none min-w-12' }"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,9 @@ const appConfig = useAppConfig()
|
||||
<UFormField
|
||||
label="toaster.expand"
|
||||
size="sm"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
|
||||
:ui="{
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] 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 rounded-l-none min-w-12"
|
||||
class="rounded-[var(--ui-radius)] rounded-l-none min-w-12"
|
||||
:search-input="false"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
@@ -10,9 +10,9 @@ const appConfig = useAppConfig()
|
||||
<UFormField
|
||||
label="toaster.position"
|
||||
size="sm"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
|
||||
:ui="{
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] 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 rounded-l-none min-w-12"
|
||||
class="rounded-[var(--ui-radius)] rounded-l-none min-w-12"
|
||||
:search-input="false"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
@@ -14,7 +14,9 @@ select:
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install the Nuxt UI v3 alpha package:
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the Nuxt UI v3 alpha package
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
@@ -37,10 +39,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` directly 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` in your project's root directory.
|
||||
::
|
||||
|
||||
2. Register the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}:
|
||||
#### Add the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
@@ -48,15 +50,24 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
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):
|
||||
#### Import Tailwind CSS and Nuxt UI in your CSS
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [assets/css/main.css]
|
||||
@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
|
||||
@@ -70,8 +81,6 @@ 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
|
||||
|
||||
@@ -14,7 +14,9 @@ select:
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install the Nuxt UI v3 alpha package:
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the Nuxt UI v3 alpha package
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
@@ -37,12 +39,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` directly 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` in your project's root directory.
|
||||
::
|
||||
|
||||
2. Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}:
|
||||
#### Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}
|
||||
|
||||
```ts [vite.config.ts]
|
||||
```ts [vite.config.ts]{3,8}
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import ui from '@nuxt/ui/vite'
|
||||
@@ -51,7 +53,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
ui()
|
||||
],
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
@@ -71,28 +73,45 @@ components.d.ts
|
||||
```
|
||||
::
|
||||
|
||||
3. Register the Nuxt UI Vue plugin in your app:
|
||||
#### Use the Nuxt UI Vue plugin in your `main.ts`
|
||||
|
||||
```ts [main.ts]
|
||||
```ts [main.ts]{2,7}
|
||||
import { createApp } from 'vue'
|
||||
import nuxtUI from '@nuxt/ui/vue-plugin'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
// ...
|
||||
app.use(nuxtUI)
|
||||
|
||||
app.use(ui)
|
||||
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
4. Import Tailwind CSS and Nuxt UI in your `App.vue`{lang="ts-type"} or CSS:
|
||||
#### Import Tailwind CSS and Nuxt UI in your CSS
|
||||
|
||||
```vue [App.vue]
|
||||
<style>
|
||||
```css [assets/main.css]
|
||||
@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
|
||||
@@ -106,8 +125,6 @@ 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
|
||||
|
||||
@@ -11,13 +11,12 @@ 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:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme {
|
||||
--font-family-sans: 'Public Sans', sans-serif;
|
||||
--font-sans: 'Public Sans', sans-serif;
|
||||
|
||||
--breakpoint-3xl: 1920px;
|
||||
|
||||
@@ -33,7 +32,6 @@ 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.
|
||||
@@ -48,13 +46,11 @@ 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):
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@source "../content/**/*.md";
|
||||
</style>
|
||||
```
|
||||
|
||||
::note{to="https://github.com/tailwindlabs/tailwindcss/pull/14078"}
|
||||
@@ -65,13 +61,11 @@ You can learn more about the `@source` directive in this pull request.
|
||||
|
||||
You can use the `@plugin` directive to import Tailwind CSS plugins.
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
</style>
|
||||
```
|
||||
|
||||
::note{to="https://github.com/tailwindlabs/tailwindcss/pull/14264"}
|
||||
@@ -154,8 +148,7 @@ 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:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -172,7 +165,6 @@ 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>
|
||||
```
|
||||
::
|
||||
|
||||
@@ -217,8 +209,7 @@ 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:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -229,7 +220,6 @@ You can change which shade is used for each color on light and dark mode:
|
||||
.dark {
|
||||
--ui-primary: var(--ui-color-primary-200);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
::
|
||||
|
||||
@@ -324,8 +314,7 @@ body {
|
||||
::tip
|
||||
You can customize these CSS variables to tailor the appearance of your application:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -338,7 +327,6 @@ 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>
|
||||
```
|
||||
::
|
||||
|
||||
@@ -359,15 +347,40 @@ 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:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
:root {
|
||||
--ui-radius: var(--radius-sm);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
::
|
||||
|
||||
#### 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);
|
||||
}
|
||||
```
|
||||
::
|
||||
|
||||
@@ -384,7 +397,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',
|
||||
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow-sm',
|
||||
header: 'p-4 sm:px-6',
|
||||
body: 'p-4 sm:p-6',
|
||||
footer: 'p-4 sm:px-6'
|
||||
@@ -418,7 +431,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-7xl mx-auto px-4 sm:px-6 lg:px-8'
|
||||
base: 'max-w-[var(--ui-container)] mx-auto px-4 sm:px-6 lg:px-8'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -12,15 +12,13 @@ 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:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme {
|
||||
--font-family-sans: 'Public Sans', sans-serif;
|
||||
--font-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.
|
||||
|
||||
1
docs/content/1.getting-started/7.i18n/.navigation.yml
Normal file
1
docs/content/1.getting-started/7.i18n/.navigation.yml
Normal file
@@ -0,0 +1 @@
|
||||
badge: New
|
||||
167
docs/content/1.getting-started/7.i18n/1.nuxt.md
Normal file
167
docs/content/1.getting-started/7.i18n/1.nuxt.md
Normal file
@@ -0,0 +1,167 @@
|
||||
---
|
||||
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
|
||||
180
docs/content/1.getting-started/7.i18n/2.vue.md
Normal file
180
docs/content/1.getting-started/7.i18n/2.vue.md
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
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
|
||||
@@ -27,6 +27,10 @@ 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
|
||||
|
||||
@@ -67,7 +67,7 @@ class: 'p-8'
|
||||
|
||||
### Prev / Next
|
||||
|
||||
Use the `prev` and `next` props to customize the prev and next buttons.
|
||||
Use the `prev` and `next` props to customize the prev and next buttons with any [Button](/components/button) props.
|
||||
|
||||
::component-example
|
||||
---
|
||||
@@ -76,7 +76,7 @@ class: 'p-8'
|
||||
---
|
||||
::
|
||||
|
||||
### Prev Icon / Next Icon
|
||||
### Prev / Next Icons
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ It requires two props:
|
||||
::
|
||||
::
|
||||
|
||||
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"}.
|
||||
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"}.
|
||||
|
||||
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"}.
|
||||
|
||||
|
||||
@@ -214,6 +214,42 @@ 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.
|
||||
@@ -694,7 +730,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.
|
||||
Use the `filter` prop with an array of fields to filter on. Defaults to `[labelKey]`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
@@ -703,6 +739,17 @@ 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
|
||||
|
||||
291
docs/content/3.components/input-number.md
Normal file
291
docs/content/3.components/input-number.md
Normal file
@@ -0,0 +1,291 @@
|
||||
---
|
||||
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
|
||||
@@ -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), 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), [InputNumber](/components/input-number) etc. and others have been styled like `file` for example.
|
||||
|
||||
::component-code
|
||||
---
|
||||
items:
|
||||
type:
|
||||
- text
|
||||
- password
|
||||
- number
|
||||
- password
|
||||
- search
|
||||
- file
|
||||
props:
|
||||
@@ -214,6 +214,16 @@ 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.
|
||||
@@ -245,6 +255,20 @@ 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.
|
||||
|
||||
@@ -13,6 +13,7 @@ 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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
|
||||
::component-code
|
||||
---
|
||||
@@ -152,6 +152,7 @@ props:
|
||||
items:
|
||||
- - label: Guide
|
||||
icon: i-lucide-book-open
|
||||
defaultOpen: true
|
||||
children:
|
||||
- label: Introduction
|
||||
description: Fully styled and customizable components for Nuxt.
|
||||
|
||||
182
docs/content/3.components/pin-input.md
Normal file
182
docs/content/3.components/pin-input.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
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
|
||||
@@ -200,7 +200,9 @@ props:
|
||||
|
||||
### Search Input
|
||||
|
||||
Use the `search-input` prop to customize the search input. Defaults to `{ placeholder: 'Search...' }`{lang="ts-type"}.
|
||||
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.
|
||||
|
||||
::component-code
|
||||
---
|
||||
@@ -219,6 +221,7 @@ props:
|
||||
icon: 'i-lucide-circle-help'
|
||||
searchInput:
|
||||
placeholder: 'Filter...'
|
||||
icon: 'i-lucide-search'
|
||||
items:
|
||||
- label: Backlog
|
||||
icon: 'i-lucide-circle-help'
|
||||
@@ -236,6 +239,44 @@ 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.
|
||||
@@ -732,7 +773,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.
|
||||
Use the `filter` prop with an array of fields to filter on. Defaults to `[labelKey]`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
@@ -741,6 +782,17 @@ 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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createResolver } from '@nuxt/kit'
|
||||
import module from '../src/module'
|
||||
import pkg from '../package.json'
|
||||
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
@@ -10,7 +9,7 @@ export default defineNuxtConfig({
|
||||
// ],
|
||||
|
||||
modules: [
|
||||
module,
|
||||
'../src/module',
|
||||
'@nuxt/ui-pro',
|
||||
'@nuxt/content',
|
||||
'@nuxt/image',
|
||||
@@ -57,6 +56,7 @@ 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,7 +70,8 @@ export default defineNuxtConfig({
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: [
|
||||
'/getting-started'
|
||||
'/getting-started',
|
||||
'/api/countries.json'
|
||||
// '/api/releases.json',
|
||||
// '/api/pulls.json'
|
||||
],
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
"name": "@nuxt/ui-docs",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.12",
|
||||
"@iconify-json/logos": "^1.2.3",
|
||||
"@iconify-json/lucide": "^1.2.15",
|
||||
"@iconify-json/simple-icons": "^1.2.11",
|
||||
"@iconify-json/vscode-icons": "^1.2.2",
|
||||
"@nuxt/content": "3.0.0-alpha.5",
|
||||
"@nuxt/content": "3.0.0-alpha.6",
|
||||
"@nuxt/image": "^1.8.1",
|
||||
"@nuxt/ui": "latest",
|
||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@62862c8",
|
||||
"@nuxthub/core": "^0.8.6",
|
||||
"@nuxtjs/plausible": "^1.0.3",
|
||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@574082c",
|
||||
"@nuxthub/core": "^0.8.7",
|
||||
"@nuxtjs/plausible": "^1.1.1",
|
||||
"@octokit/rest": "^21.0.2",
|
||||
"@vueuse/nuxt": "^11.2.0",
|
||||
"joi": "^17.13.3",
|
||||
@@ -27,6 +28,6 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.85.0"
|
||||
"wrangler": "^3.87.0"
|
||||
}
|
||||
}
|
||||
|
||||
202
docs/server/api/countries.json.get.ts
Normal file
202
docs/server/api/countries.json.get.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
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)
|
||||
58
package.json
58
package.json
@@ -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.8",
|
||||
"packageManager": "pnpm@9.12.3",
|
||||
"version": "3.0.0-alpha.9",
|
||||
"packageManager": "pnpm@9.13.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/ui.git"
|
||||
@@ -30,7 +30,11 @@
|
||||
"./vue-plugin": {
|
||||
"types": "./vue-plugin.d.ts"
|
||||
},
|
||||
"./runtime/*": "./dist/runtime/*"
|
||||
"./runtime/*": "./dist/runtime/*",
|
||||
"./locale": {
|
||||
"types": "./dist/runtime/locale/index.d.ts",
|
||||
"import": "./dist/runtime/locale/index.js"
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
"#build/ui/*": "./.nuxt/ui/*.ts"
|
||||
@@ -51,6 +55,7 @@
|
||||
"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",
|
||||
@@ -64,58 +69,60 @@
|
||||
"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.6.1",
|
||||
"@nuxt/icon": "^1.8.1",
|
||||
"@nuxt/kit": "^3.14.159",
|
||||
"@nuxt/schema": "^3.14.159",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@tailwindcss/postcss": "4.0.0-alpha.30",
|
||||
"@tailwindcss/vite": "4.0.0-alpha.30",
|
||||
"@tailwindcss/postcss": "4.0.0-alpha.34",
|
||||
"@tailwindcss/vite": "4.0.0-alpha.34",
|
||||
"@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.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-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-wheel-gestures": "^8.0.1",
|
||||
"fuse.js": "^7.0.0",
|
||||
"get-port-please": "^3.1.2",
|
||||
"knitwork": "^1.1.0",
|
||||
"magic-string": "^0.30.12",
|
||||
"mlly": "^1.7.2",
|
||||
"magic-string": "^0.30.13",
|
||||
"mlly": "^1.7.3",
|
||||
"ohash": "^1.1.4",
|
||||
"pathe": "^1.1.2",
|
||||
"radix-vue": "^1.9.8",
|
||||
"radix-vue": "^1.9.10",
|
||||
"scule": "^1.3.0",
|
||||
"sirv": "^3.0.0",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss": "4.0.0-alpha.30",
|
||||
"tailwind-variants": "^0.3.0",
|
||||
"tailwindcss": "4.0.0-alpha.34",
|
||||
"tinyglobby": "^0.2.10",
|
||||
"unplugin": "^1.15.0",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin": "^1.16.0",
|
||||
"unplugin-auto-import": "^0.18.5",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vaul-vue": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint-config": "^0.6.1",
|
||||
"@nuxt/eslint-config": "^0.7.1",
|
||||
"@nuxt/module-builder": "^0.8.4",
|
||||
"@nuxt/test-utils": "^3.14.4",
|
||||
"@release-it/conventional-changelog": "^9.0.2",
|
||||
"@standard-schema/spec": "1.0.0-beta.1",
|
||||
"@release-it/conventional-changelog": "^9.0.3",
|
||||
"@standard-schema/spec": "1.0.0-beta.3",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"embla-carousel": "^8.3.1",
|
||||
"eslint": "^9.14.0",
|
||||
"embla-carousel": "^8.4.0",
|
||||
"eslint": "^9.15.0",
|
||||
"happy-dom": "^15.7.4",
|
||||
"joi": "^17.13.3",
|
||||
"knitwork": "^1.1.0",
|
||||
@@ -124,7 +131,7 @@
|
||||
"release-it": "^17.10.0",
|
||||
"superstruct": "^2.0.2",
|
||||
"valibot": "^0.42.1",
|
||||
"vitest": "^2.1.4",
|
||||
"vitest": "^2.1.5",
|
||||
"vitest-environment-nuxt": "^1.0.1",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"yup": "^1.4.0",
|
||||
@@ -135,6 +142,7 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@nuxt/ui": "workspace:*",
|
||||
"@nuxt/content": "3.0.0-alpha.5",
|
||||
"happy-dom": "14.12.3",
|
||||
"rollup": "^4.24.0"
|
||||
}
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/ui": "latest",
|
||||
"vue": "^3.5.12",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-vue": "^5.2.0",
|
||||
"typescript": "^5.6.3",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-auto-import": "^0.18.5",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.10",
|
||||
"vite": "^5.4.11",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
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,
|
||||
@@ -43,6 +45,7 @@ const components = [
|
||||
'modal',
|
||||
'navigation-menu',
|
||||
'pagination',
|
||||
'pin-input',
|
||||
'popover',
|
||||
'progress',
|
||||
'radio-group',
|
||||
@@ -83,6 +86,16 @@ 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 />
|
||||
@@ -103,7 +116,7 @@ defineShortcuts({
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme {
|
||||
--font-family-sans: 'Public Sans', sans-serif;
|
||||
--font-sans: 'Public Sans', sans-serif;
|
||||
|
||||
--color-green-50: #EFFDF5;
|
||||
--color-green-100: #D9FBE8;
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { splitByCase, upperFirst } from 'scule'
|
||||
import { useColorMode } from '#imports'
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
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 components = [
|
||||
'accordion',
|
||||
@@ -25,11 +38,13 @@ const components = [
|
||||
'form-field',
|
||||
'input',
|
||||
'input-menu',
|
||||
'input-number',
|
||||
'kbd',
|
||||
'link',
|
||||
'modal',
|
||||
'navigation-menu',
|
||||
'pagination',
|
||||
'pin-input',
|
||||
'popover',
|
||||
'progress',
|
||||
'radio-group',
|
||||
@@ -72,6 +87,22 @@ 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>
|
||||
@@ -94,7 +125,7 @@ defineShortcuts({
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme {
|
||||
--font-family-sans: 'Public Sans', sans-serif;
|
||||
--font-sans: 'Public Sans', sans-serif;
|
||||
|
||||
--color-green-50: #EFFDF5;
|
||||
--color-green-100: #D9FBE8;
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
<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>
|
||||
@@ -1,65 +0,0 @@
|
||||
<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>
|
||||
@@ -1,83 +0,0 @@
|
||||
<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>
|
||||
@@ -1,6 +1,9 @@
|
||||
<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(),
|
||||
@@ -8,18 +11,20 @@ const schema = z.object({
|
||||
tos: z.literal(true)
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
type Schema = z.input<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-4">
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex gap-4">
|
||||
<UForm
|
||||
:state="state"
|
||||
@@ -40,75 +45,24 @@ function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
</UFormField>
|
||||
|
||||
<div>
|
||||
<UButton color="neutral" type="submit">
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
|
||||
<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 />
|
||||
<FormExampleNested />
|
||||
<FormExampleNestedList />
|
||||
</div>
|
||||
|
||||
<USeparator class="my-8" />
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<FormExampleElements :validate-on="validateOn" :disabled="disabled" class="border-t border-[var(--ui-border)] p-4" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
68
playground/app/pages/components/input-number.vue
Normal file
68
playground/app/pages/components/input-number.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<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>
|
||||
@@ -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('horizontal' as const)
|
||||
const orientation = ref('vertical' as const)
|
||||
const highlight = ref(true)
|
||||
|
||||
const items = [
|
||||
@@ -16,6 +16,7 @@ const items = [
|
||||
label: 'Documentation',
|
||||
icon: 'i-lucide-book-open',
|
||||
badge: 10,
|
||||
defaultOpen: true,
|
||||
children: [{
|
||||
label: 'Introduction',
|
||||
description: 'Fully styled and customizable components for Nuxt.',
|
||||
|
||||
52
playground/app/pages/components/pin-input.vue
Normal file
52
playground/app/pages/components/pin-input.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<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>
|
||||
@@ -8,7 +8,8 @@
|
||||
"generate": "nuxi generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.12",
|
||||
"@iconify-json/lucide": "^1.2.15",
|
||||
"@iconify-json/simple-icons": "^1.2.11",
|
||||
"@nuxt/ui": "latest",
|
||||
"nuxt": "^3.14.159"
|
||||
}
|
||||
|
||||
3315
pnpm-lock.yaml
generated
3315
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
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)
|
||||
})
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/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
|
||||
@@ -176,16 +176,18 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
|
||||
nuxt.options.routeRules = defu(nuxt.options.routeRules, { '/__nuxt_ui__/**': { ssr: false } })
|
||||
extendPages((pages) => {
|
||||
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
|
||||
}
|
||||
})
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
addCustomTab({
|
||||
|
||||
@@ -28,6 +28,7 @@ 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[]
|
||||
/**
|
||||
@@ -65,6 +66,7 @@ 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'
|
||||
@@ -74,6 +76,7 @@ const emits = defineEmits<AlertEmits>()
|
||||
const slots = defineSlots<AlertSlots>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
|
||||
const multiline = computed(() => !!props.title && !!props.description)
|
||||
|
||||
@@ -123,7 +126,7 @@ const ui = computed(() => alert({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
aria-label="Close"
|
||||
:aria-label="t('alert.close')"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
@click="emits('update:open', false)"
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { ConfigProviderProps, TooltipProviderProps } from 'radix-vue'
|
||||
import { localeContextInjectionKey } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { ToasterProps } from '../types'
|
||||
import type { ToasterProps, Locale } from '../types'
|
||||
import { en } from '../locale'
|
||||
|
||||
export interface AppProps extends Omit<ConfigProviderProps, 'useId'> {
|
||||
export interface AppProps extends Omit<ConfigProviderProps, 'useId' | 'dir'> {
|
||||
tooltip?: TooltipProviderProps
|
||||
toaster?: ToasterProps | null
|
||||
locale?: Locale
|
||||
}
|
||||
|
||||
export interface AppSlots {
|
||||
@@ -20,7 +23,7 @@ extendDevtoolsMeta({ ignore: true })
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRef, useId } from 'vue'
|
||||
import { toRef, useId, provide, computed } from 'vue'
|
||||
import { ConfigProvider, TooltipProvider, useForwardProps } from 'radix-vue'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import UToaster from './Toaster.vue'
|
||||
@@ -30,13 +33,16 @@ import USlideoverProvider from './SlideoverProvider.vue'
|
||||
const props = defineProps<AppProps>()
|
||||
defineSlots<AppSlots>()
|
||||
|
||||
const configProviderProps = useForwardProps(reactivePick(props, 'dir', 'scrollBody'))
|
||||
const configProviderProps = useForwardProps(reactivePick(props, '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)" v-bind="configProviderProps">
|
||||
<ConfigProvider :use-id="() => (useId() as string)" :dir="locale.dir" v-bind="configProviderProps">
|
||||
<TooltipProvider v-bind="tooltipProps">
|
||||
<UToaster v-if="toaster !== null" v-bind="toasterProps">
|
||||
<slot />
|
||||
|
||||
@@ -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 'div'
|
||||
* @defaultValue 'nav'
|
||||
*/
|
||||
as?: any
|
||||
items?: T[]
|
||||
@@ -76,8 +76,10 @@ 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'
|
||||
@@ -86,12 +88,15 @@ 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))
|
||||
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
const ui = breadcrumb()
|
||||
</script>
|
||||
@@ -123,7 +128,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 || appConfig.ui.icons.chevronRight" :class="ui.separatorIcon({ class: props.ui?.separatorIcon })" />
|
||||
<UIcon :name="separatorIcon" :class="ui.separatorIcon({ class: props.ui?.separatorIcon })" />
|
||||
</slot>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -100,6 +100,7 @@ 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>>(), {
|
||||
@@ -134,8 +135,12 @@ 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
|
||||
}))
|
||||
@@ -144,8 +149,7 @@ const options = computed<EmblaOptionsType>(() => ({
|
||||
...(props.fade ? { align: 'center', containScroll: false } : {}),
|
||||
...rootProps.value,
|
||||
axis: props.orientation === 'horizontal' ? 'x' : 'y',
|
||||
// TODO: Get from ConfigProvider
|
||||
direction: 'ltr'
|
||||
direction: dir.value === 'rtl' ? 'rtl' : 'ltr'
|
||||
}))
|
||||
|
||||
const plugins = computedAsync<EmblaPluginType[]>(async () => {
|
||||
@@ -275,22 +279,22 @@ defineExpose({
|
||||
<div v-if="arrows" :class="ui.arrows({ class: props.ui?.arrows })">
|
||||
<UButton
|
||||
:disabled="!canScrollPrev"
|
||||
:icon="prevIcon || appConfig.ui.icons.arrowLeft"
|
||||
:icon="prevIcon"
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
aria-label="Prev"
|
||||
:aria-label="t('carousel.prev')"
|
||||
v-bind="typeof prev === 'object' ? prev : undefined"
|
||||
:class="ui.prev({ class: props.ui?.prev })"
|
||||
@click="scrollPrev"
|
||||
/>
|
||||
<UButton
|
||||
:disabled="!canScrollNext"
|
||||
:icon="nextIcon || appConfig.ui.icons.arrowRight"
|
||||
:icon="nextIcon"
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
aria-label="Next"
|
||||
:aria-label="t('carousel.next')"
|
||||
v-bind="typeof next === 'object' ? next : undefined"
|
||||
:class="ui.next({ class: props.ui?.next })"
|
||||
@click="scrollNext"
|
||||
@@ -300,7 +304,7 @@ defineExpose({
|
||||
<div v-if="dots" :class="ui.dots({ class: props.ui?.dots })">
|
||||
<template v-for="(_, index) in scrollSnaps" :key="index">
|
||||
<button
|
||||
:aria-label="`Go to slide ${index + 1}`"
|
||||
:aria-label="t('carousel.goto', { slide: index + 1 })"
|
||||
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
|
||||
@click="scrollTo(index)"
|
||||
/>
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface CommandPaletteGroup<T> {
|
||||
slot?: string
|
||||
items?: T[]
|
||||
/**
|
||||
* Wether to filter group items with [useFuse](https://vueuse.org/integrations/useFuse).
|
||||
* Whether 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,6 +124,7 @@ 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'
|
||||
@@ -144,6 +145,7 @@ 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'))
|
||||
|
||||
@@ -245,7 +247,7 @@ const groups = computed(() => {
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
aria-label="Close"
|
||||
:aria-label="t('commandPalette.close')"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
@click="emits('update:open', false)"
|
||||
@@ -259,7 +261,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 ? `No results for ${searchTerm}` : 'No results' }}
|
||||
{{ searchTerm ? t('commandPalette.noMatch', { searchTerm }) : t('commandPalette.noData') }}
|
||||
</slot>
|
||||
</ComboboxEmpty>
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ 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>()
|
||||
|
||||
@@ -40,6 +40,8 @@ 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>()
|
||||
|
||||
@@ -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 { getYupErrors, isYupSchema, getValibotErrors, isValibotSchema, getZodErrors, isZodSchema, getJoiErrors, isJoiSchema, getStandardErrors, isStandardSchema, getSuperStructErrors, isSuperStructSchema } from '../utils/form'
|
||||
import { parseSchema } 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, string>>({})
|
||||
const inputs = ref<Record<string, { id?: string, pattern?: RegExp }>>({})
|
||||
provide(formInputsInjectionKey, inputs)
|
||||
|
||||
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
||||
return errs.map(err => ({
|
||||
...err,
|
||||
id: inputs.value[err.name]
|
||||
id: inputs.value[err.name]?.id
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -108,20 +108,11 @@ async function getErrors(): Promise<FormErrorWithId[]> {
|
||||
let errs = props.validate ? (await props.validate(props.state)) ?? [] : []
|
||||
|
||||
if (props.schema) {
|
||||
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))
|
||||
const { errors, result } = await parseSchema(props.state, props.schema as FormSchema<typeof props.state>)
|
||||
if (errors) {
|
||||
errs = errs.concat(errors)
|
||||
} else {
|
||||
throw new Error('Form validation failed: Unsupported form schema')
|
||||
Object.assign(props.state, result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +120,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
|
||||
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as string[]
|
||||
|
||||
const nestedValidatePromises = !names && opts.nested
|
||||
? Array.from(nestedForms.value.values()).map(
|
||||
@@ -143,9 +134,16 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes
|
||||
: []
|
||||
|
||||
if (names) {
|
||||
const otherErrors = errors.value.filter(error => !names!.includes(error.name))
|
||||
const pathErrors = (await getErrors()).filter(error => names!.includes(error.name)
|
||||
)
|
||||
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))
|
||||
}))
|
||||
|
||||
errors.value = otherErrors.concat(pathErrors)
|
||||
} else {
|
||||
errors.value = await getErrors()
|
||||
@@ -196,7 +194,7 @@ provide(formOptionsInjectionKey, computed(() => ({
|
||||
validateOnInputDelay: props.validateOnInputDelay
|
||||
})))
|
||||
|
||||
defineExpose<{ $el: HTMLFormElement | HTMLDivElement } & Form<T>>({
|
||||
defineExpose<Form<T>>({
|
||||
validate: _validate,
|
||||
errors,
|
||||
|
||||
@@ -230,7 +228,7 @@ defineExpose<{ $el: HTMLFormElement | HTMLDivElement } & Form<T>>({
|
||||
},
|
||||
|
||||
disabled
|
||||
} as { $el: HTMLFormElement | HTMLDivElement } & Form<T>)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -12,7 +12,10 @@ 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
|
||||
@@ -54,7 +57,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)?.message)
|
||||
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern)))?.message)
|
||||
|
||||
const id = ref(useId())
|
||||
|
||||
@@ -65,7 +68,8 @@ provide(formFieldInjectionKey, computed(() => ({
|
||||
name: props.name,
|
||||
size: props.size,
|
||||
eagerValidation: props.eagerValidation,
|
||||
validateOnInputDelay: props.validateOnInputDelay
|
||||
validateOnInputDelay: props.validateOnInputDelay,
|
||||
errorPattern: props.errorPattern
|
||||
}) as FormFieldInjectedOptions<FormFieldProps>))
|
||||
</script>
|
||||
|
||||
@@ -92,7 +96,7 @@ provide(formFieldInjectionKey, computed(() => ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div :class="[label && ui.container({ class: props.ui?.container })]">
|
||||
<div :class="[(label || !!slots.label || description || !!slots.description) && 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 })">
|
||||
|
||||
@@ -78,9 +78,10 @@ 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.
|
||||
* When `false`, items will not be filtered which is useful for custom filtering.
|
||||
* @defaultValue ['label']
|
||||
* 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
|
||||
*/
|
||||
filter?: boolean | string[]
|
||||
/**
|
||||
@@ -96,6 +97,11 @@ 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`. */
|
||||
@@ -108,6 +114,7 @@ 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
|
||||
@@ -122,21 +129,23 @@ 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 } from 'vue'
|
||||
import { computed, ref, toRef, onMounted, toRaw } 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 } from '@vueuse/core'
|
||||
import { reactivePick, createReusableTemplate } 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'
|
||||
@@ -148,7 +157,7 @@ const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
|
||||
type: 'text',
|
||||
autofocusDelay: 0,
|
||||
portal: true,
|
||||
filter: () => ['label'],
|
||||
filter: true,
|
||||
labelKey: 'label' as never
|
||||
})
|
||||
const emits = defineEmits<InputMenuEmits<T, V, M>>()
|
||||
@@ -157,6 +166,7 @@ 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)
|
||||
@@ -167,6 +177,8 @@ 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({
|
||||
@@ -191,23 +203,27 @@ function displayValue(value: T): string {
|
||||
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
||||
}
|
||||
|
||||
function filterFunction(items: ArrayOrWrapped<T>, searchTerm: string): ArrayOrWrapped<T> {
|
||||
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> {
|
||||
if (props.filter === false) {
|
||||
return items
|
||||
return inputItems
|
||||
}
|
||||
|
||||
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
|
||||
const escapedSearchTerm = escapeRegExp(searchTerm)
|
||||
const escapedSearchTerm = escapeRegExp(filterSearchTerm ?? '')
|
||||
|
||||
return items.filter((item) => {
|
||||
return inputItems.filter((item) => {
|
||||
if (typeof item !== 'object') {
|
||||
return String(item).search(new RegExp(escapedSearchTerm, 'i')) !== -1
|
||||
return comparator(item, escapedSearchTerm)
|
||||
}
|
||||
|
||||
return fields.some((field) => {
|
||||
const child = get(item, field as string)
|
||||
|
||||
return child !== null && child !== undefined && String(child).search(new RegExp(escapedSearchTerm, 'i')) !== -1
|
||||
return child !== null && child !== undefined && comparator(child, escapedSearchTerm)
|
||||
})
|
||||
}) as ArrayOrWrapped<T>
|
||||
}
|
||||
@@ -216,6 +232,36 @@ 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() {
|
||||
@@ -231,6 +277,9 @@ 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)
|
||||
@@ -264,6 +313,22 @@ 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 }"
|
||||
@@ -273,7 +338,7 @@ defineExpose({
|
||||
:disabled="disabled"
|
||||
:multiple="multiple"
|
||||
:display-value="displayValue"
|
||||
:filter-function="filterFunction"
|
||||
:filter-function="() => rootItems"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:as-child="!!multiple"
|
||||
@update:model-value="onUpdate"
|
||||
@@ -347,11 +412,13 @@ 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 ? `No results for ${searchTerm}` : 'No results' }}
|
||||
{{ searchTerm ? t('inputMenu.noMatch', { searchTerm }) : t('inputMenu.noData') }}
|
||||
</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 })">
|
||||
@@ -398,6 +465,8 @@ 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 })" />
|
||||
|
||||
192
src/runtime/components/InputNumber.vue
Normal file
192
src/runtime/components/InputNumber.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<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>
|
||||
@@ -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
|
||||
/** Will only be active if the current route query is an exact match. */
|
||||
exactQuery?: boolean
|
||||
/** Allows controlling how the current route query sets the link as active. */
|
||||
exactQuery?: boolean | 'partial'
|
||||
/** Will only be active if the current route hash is an exact match. */
|
||||
exactHash?: boolean
|
||||
/** The class to apply when the link is inactive. */
|
||||
@@ -94,10 +94,11 @@ extendDevtoolsMeta({ example: 'LinkExample' })
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { isEqual } from 'ohash'
|
||||
import { isEqual, diff } from 'ohash'
|
||||
import { useForwardProps } from 'radix-vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { useRoute } from '#imports'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
@@ -123,14 +124,27 @@ 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 && !isEqual(linkRoute.query, route.query)) {
|
||||
return false
|
||||
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.exactHash && linkRoute.hash !== route.hash) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ 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>(), {
|
||||
@@ -95,14 +96,22 @@ const contentEvents = computed(() => {
|
||||
if (props.preventClose) {
|
||||
return {
|
||||
pointerDownOutside: (e: Event) => e.preventDefault(),
|
||||
interactOutside: (e: Event) => e.preventDefault()
|
||||
interactOutside: (e: Event) => e.preventDefault(),
|
||||
escapeKeyDown: (e: Event) => e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
return {
|
||||
interactOutside: (e: Event) => {
|
||||
if (e.target instanceof Element && e.target.closest('[data-sonner-toaster]')) {
|
||||
return e.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
|
||||
const ui = computed(() => modal({
|
||||
transition: props.transition,
|
||||
@@ -143,7 +152,7 @@ const ui = computed(() => modal({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
aria-label="Close"
|
||||
:aria-label="t('modal.close')"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { tv, type VariantProps } from 'tailwind-variants'
|
||||
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps } from 'radix-vue'
|
||||
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, CollapsibleRootProps } 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'> {
|
||||
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
|
||||
label?: string
|
||||
icon?: string
|
||||
avatar?: AvatarProps
|
||||
@@ -208,6 +208,8 @@ 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>
|
||||
|
||||
@@ -103,9 +103,11 @@ 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>(), {
|
||||
@@ -124,9 +126,14 @@ 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))
|
||||
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
const ui = pagination()
|
||||
</script>
|
||||
@@ -136,12 +143,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 || appConfig.ui.icons.chevronDoubleLeft" :to="to?.(1)" />
|
||||
<UButton :color="color" :variant="variant" :size="size" :icon="firstIcon" :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 || appConfig.ui.icons.chevronLeft" :to="page > 1 ? to?.(page - 1) : undefined" />
|
||||
<UButton :color="color" :variant="variant" :size="size" :icon="prevIcon" :to="page > 1 ? to?.(page - 1) : undefined" />
|
||||
</slot>
|
||||
</PaginationPrev>
|
||||
|
||||
@@ -169,12 +176,12 @@ const ui = pagination()
|
||||
|
||||
<PaginationNext v-if="showControls || !!slots.next" as-child>
|
||||
<slot name="next">
|
||||
<UButton :color="color" :variant="variant" :size="size" :icon="nextIcon || appConfig.ui.icons.chevronRight" :to="page < pageCount ? to?.(pageCount) : undefined" />
|
||||
<UButton :color="color" :variant="variant" :size="size" :icon="nextIcon" :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 || appConfig.ui.icons.chevronDoubleRight" :to=" to?.(pageCount)" />
|
||||
<UButton :color="color" :variant="variant" :size="size" :icon="lastIcon" :to=" to?.(pageCount)" />
|
||||
</slot>
|
||||
</PaginationLast>
|
||||
</PaginationList>
|
||||
|
||||
96
src/runtime/components/PinInput.vue
Normal file
96
src/runtime/components/PinInput.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<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>
|
||||
@@ -31,6 +31,11 @@ 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>
|
||||
}
|
||||
@@ -64,6 +69,17 @@ 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
|
||||
@@ -81,7 +97,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] })">
|
||||
<Component.Content v-bind="contentProps" :class="ui.content({ class: [props.class, props.ui?.content] })" v-on="contentEvents">
|
||||
<slot name="content" />
|
||||
|
||||
<Component.Arrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />
|
||||
|
||||
@@ -34,11 +34,12 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
|
||||
/** The placeholder text when the select is empty. */
|
||||
placeholder?: string
|
||||
/**
|
||||
* Wether to display the search input or not.
|
||||
* Whether to display the search input or not.
|
||||
* Can be an object to pass additional props to the input.
|
||||
* @defaultValue { placeholder: 'Search...' }
|
||||
* `{ placeholder: 'Search...', variant: 'none' }`{lang="ts-type"}
|
||||
* @defaultValue true
|
||||
*/
|
||||
searchInput?: boolean | { placeholder?: string }
|
||||
searchInput?: boolean | InputProps
|
||||
color?: SelectMenuVariants['color']
|
||||
variant?: SelectMenuVariants['variant']
|
||||
size?: SelectMenuVariants['size']
|
||||
@@ -69,9 +70,10 @@ 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.
|
||||
* 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.).
|
||||
* @defaultValue ['label']
|
||||
* `['label']`{lang="ts-type"}
|
||||
* @defaultValue true
|
||||
*/
|
||||
filter?: boolean | string[]
|
||||
/**
|
||||
@@ -87,6 +89,11 @@ 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`. */
|
||||
@@ -99,6 +106,7 @@ 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
|
||||
@@ -112,32 +120,35 @@ 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 } from 'vue'
|
||||
import { computed, toRef, toRaw } 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 } from '@vueuse/core'
|
||||
import { reactivePick, createReusableTemplate } 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: () => ({ placeholder: 'Search...' }),
|
||||
filter: () => ['label'],
|
||||
searchInput: true,
|
||||
filter: true,
|
||||
labelKey: 'label' as never
|
||||
})
|
||||
|
||||
@@ -147,12 +158,17 @@ 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 })))
|
||||
@@ -179,28 +195,32 @@ 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))
|
||||
const item = items.value.find(item => isEqual(get(item as Record<string, any>, props.valueKey as string), value)) ?? (props.createItem && value)
|
||||
|
||||
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
||||
}
|
||||
|
||||
function filterFunction(items: ArrayOrWrapped<T>, searchTerm: string): ArrayOrWrapped<T> {
|
||||
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> {
|
||||
if (props.filter === false) {
|
||||
return items
|
||||
return inputItems
|
||||
}
|
||||
|
||||
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
|
||||
const escapedSearchTerm = escapeRegExp(searchTerm)
|
||||
const escapedSearchTerm = escapeRegExp(filterSearchTerm)
|
||||
|
||||
return items.filter((item: T) => {
|
||||
return inputItems.filter((item: T) => {
|
||||
if (typeof item !== 'object') {
|
||||
return String(item).search(new RegExp(escapedSearchTerm, 'i')) !== -1
|
||||
return comparator(item, escapedSearchTerm)
|
||||
}
|
||||
|
||||
return fields.some((field) => {
|
||||
const child = get(item, field as string)
|
||||
|
||||
return child !== null && child !== undefined && String(child).search(new RegExp(escapedSearchTerm, 'i')) !== -1
|
||||
return child !== null && child !== undefined && comparator(child, escapedSearchTerm)
|
||||
})
|
||||
}) as ArrayOrWrapped<T>
|
||||
}
|
||||
@@ -209,7 +229,40 @@ 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)
|
||||
@@ -230,6 +283,22 @@ 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 }"
|
||||
@@ -240,7 +309,7 @@ function onUpdateOpen(value: boolean) {
|
||||
:disabled="disabled"
|
||||
:multiple="multiple"
|
||||
:display-value="() => searchTerm"
|
||||
:filter-function="filterFunction"
|
||||
:filter-function="() => rootItems"
|
||||
@update:model-value="onUpdate"
|
||||
@update:open="onUpdateOpen"
|
||||
>
|
||||
@@ -274,21 +343,19 @@ function onUpdateOpen(value: boolean) {
|
||||
|
||||
<ComboboxPortal :disabled="!portal">
|
||||
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
|
||||
<ComboboxInput
|
||||
v-if="!!searchInput"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
v-bind="typeof searchInput === 'object' ? searchInput : {}"
|
||||
:class="ui.input({ class: props.ui?.input })"
|
||||
/>
|
||||
<ComboboxInput v-if="!!searchInput" as-child>
|
||||
<UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
|
||||
</ComboboxInput>
|
||||
|
||||
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
|
||||
<slot name="empty" :search-term="searchTerm">
|
||||
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
|
||||
{{ searchTerm ? t('selectMenu.noMatch', { searchTerm }) : t('selectMenu.noData') }}
|
||||
</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 })">
|
||||
@@ -335,6 +402,8 @@ 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 })" />
|
||||
|
||||
@@ -75,6 +75,7 @@ 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>(), {
|
||||
@@ -94,14 +95,22 @@ const contentEvents = computed(() => {
|
||||
if (props.preventClose) {
|
||||
return {
|
||||
pointerDownOutside: (e: Event) => e.preventDefault(),
|
||||
interactOutside: (e: Event) => e.preventDefault()
|
||||
interactOutside: (e: Event) => e.preventDefault(),
|
||||
escapeKeyDown: (e: Event) => e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
return {
|
||||
interactOutside: (e: Event) => {
|
||||
if (e.target instanceof Element && e.target.closest('[data-sonner-toaster]')) {
|
||||
return e.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
|
||||
const ui = computed(() => slideover({
|
||||
transition: props.transition,
|
||||
@@ -142,7 +151,7 @@ const ui = computed(() => slideover({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
aria-label="Close"
|
||||
:aria-label="t('slideover.close')"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
/>
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface TableData {
|
||||
export interface TableProps<T> {
|
||||
data?: T[]
|
||||
columns?: TableColumn<T>[]
|
||||
caption?: string
|
||||
/**
|
||||
* Whether the table should have a sticky header.
|
||||
* @defaultValue false
|
||||
@@ -95,6 +96,7 @@ 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>
|
||||
@@ -110,10 +112,12 @@ 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) })))
|
||||
|
||||
@@ -190,6 +194,12 @@ 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
|
||||
@@ -231,7 +241,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">
|
||||
No results
|
||||
{{ t('table.noData') }}
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -151,7 +151,7 @@ function autoResize() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => modelValue, () => {
|
||||
watch(modelValue, () => {
|
||||
nextTick(autoResize)
|
||||
})
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ 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[]
|
||||
/**
|
||||
@@ -63,6 +64,7 @@ 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'
|
||||
@@ -74,6 +76,7 @@ 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)
|
||||
@@ -151,7 +154,7 @@ defineExpose({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
aria-label="Close"
|
||||
:aria-label="t('toast.close')"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
@click.stop
|
||||
|
||||
@@ -19,6 +19,11 @@ 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>
|
||||
}
|
||||
@@ -36,7 +41,7 @@ extendDevtoolsMeta({ example: 'ToasterExample' })
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ToastProvider, ToastViewport, useForwardProps } from 'radix-vue'
|
||||
import { ToastProvider, ToastViewport, ToastPortal, useForwardProps } from 'radix-vue'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import { useToast } from '../composables/useToast'
|
||||
import { omit } from '../utils'
|
||||
@@ -44,6 +49,7 @@ import UToast from './Toast.vue'
|
||||
|
||||
const props = withDefaults(defineProps<ToasterProps>(), {
|
||||
expand: true,
|
||||
portal: true,
|
||||
duration: 5000
|
||||
})
|
||||
defineSlots<ToasterSlots>()
|
||||
@@ -120,18 +126,20 @@ function getOffset(index: number) {
|
||||
@click="toast.click && toast.click(toast)"
|
||||
/>
|
||||
|
||||
<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 :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>
|
||||
</ToastProvider>
|
||||
</template>
|
||||
|
||||
13
src/runtime/composables/defineLocale.ts
Normal file
13
src/runtime/composables/defineLocale.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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' })
|
||||
}
|
||||
@@ -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, string>>> = Symbol('nuxt-ui.form-inputs')
|
||||
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, { id?: string, pattern?: RegExp }>>> = 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] = inputId.value
|
||||
formInputs.value[formField.value.name] = { id: inputId.value, pattern: formField.value.errorPattern }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user