Compare commits

..

89 Commits

Author SHA1 Message Date
Benjamin Canac
21d8c352a9 chore(release): v3.0.0-alpha.9 2024-11-19 16:03:52 +01:00
Benjamin Canac
5deadc7096 Revert "docs(ComponentCode/ComponentExample): use relative imports"
This reverts commit d75f47419d.
2024-11-19 15:54:51 +01:00
Benjamin Canac
fa9f0a7e2a chore(Toaster): use ToastPortal from radix-vue 2024-11-19 15:41:26 +01:00
Alex
143c015bbd docs(i18n): display supported languages in a table (#2684)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-19 15:33:01 +01:00
Benjamin Canac
d75f47419d docs(ComponentCode/ComponentExample): use relative imports 2024-11-19 15:18:06 +01:00
Alex
7b148daf1f cli: fix line break doc template (#2687) 2024-11-19 14:06:48 +01:00
Benjamin Canac
30e0c7fddd docs(deps): update @nuxt/ui-pro 2024-11-19 12:24:40 +01:00
Mohet
14fb21be00 feat(locale): add Persian language (#2682)
Co-authored-by: Ali Zemani <ali.ze@arianatech.ir>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-19 12:10:09 +01:00
renovate[bot]
25091bad48 chore(deps): lock file maintenance (v3) (#2672)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-19 11:59:54 +01:00
renovate[bot]
b75ed29068 chore(deps): update all non-major dependencies (v3) (#2679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-19 11:40:06 +01:00
Thomas
b2fa65734b fix(css): --font-family-sans renamed to --font-sans (#2680)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-19 10:07:01 +01:00
Dewdew
d3a079a644 fix(Textarea): autoresize does not work when initializing modelValue (#2681) 2024-11-19 10:02:19 +01:00
renovate[bot]
8c6a8c283f chore(deps): update all non-major dependencies (v3) (#2642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 23:18:35 +01:00
kicaj
2fc36c878c feat(locale): add Polish language (#2678) 2024-11-18 22:49:05 +01:00
Aaron Dewes
992be91823 fix(locale): Improve German translation (#2676) 2024-11-18 21:59:27 +01:00
Alex
bd2f077fe8 feat(InputNumber): implement component (#2577)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-18 10:08:57 +01:00
Sandro Circi
7329900ae5 feat(Link): allow partial query match for its active state (#2664)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-17 12:16:19 +01:00
Benjamin Canac
8d85498ee1 fix(Button): improve neutral solid variant hover 2024-11-16 21:59:27 +01:00
Benjamin Canac
5c292cf620 chore(Alert/Toast): improve tsdoc 2024-11-16 14:27:16 +01:00
Benjamin Canac
c0837059a9 playground: add color mode button 2024-11-15 12:58:06 +01:00
Benjamin Canac
f5ea2411dc chore(package): add dev:vue script 2024-11-15 12:57:55 +01:00
Malik-Jouda
1fbbfe8df0 fix(Carousel): use dir from locale (#2647) 2024-11-15 12:00:51 +01:00
Sandro Circi
0daac5bafb fix(Toast): unreachable behind overlays (#2650)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-15 11:59:08 +01:00
Benjamin Canac
756f791a1a fix(Breadcrumb): render as nav
Resolves #2649
2024-11-15 09:52:44 +01:00
Eduard Aymerich
8ed434c105 feat(locale): translate Spanish (#2644) 2024-11-15 09:48:11 +01:00
renovate[bot]
190a2c9799 chore(deps): update tailwindcss to v4.0.0-alpha.34 (v3) (#2645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 09:47:16 +01:00
Alex
e55c0e2594 feat(locale): typing dir (#2643) 2024-11-14 19:53:35 +01:00
Benjamin Canac
4312ca4702 docs(deps): update @nuxt/ui-pro 2024-11-14 18:26:50 +01:00
renovate[bot]
2289742656 chore(deps): update dependency @nuxt/icon to ^1.7.5 (v3) (#2639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 16:45:20 +01:00
Benjamin Canac
601f4b2cd2 fix(PinInput): missing useFormField import 2024-11-14 15:57:21 +01:00
Benjamin Canac
cd080541a0 docs(deps): add @iconify-json/logos 2024-11-14 15:45:22 +01:00
Benjamin Canac
5722e0802d playground(deps): add @iconify-json/simple-icons 2024-11-14 15:44:35 +01:00
Benjamin Canac
8d0026558a fix(css): remove useless spacing override 2024-11-14 11:47:58 +01:00
Malik-Jouda
e5119a9ca4 fix(Breadcrumb/Carousel/Pagination): handle icons in RTL mode (#2633) 2024-11-14 10:17:55 +01:00
renovate[bot]
976dd2a386 chore(deps): update all non-major dependencies (v3) (#2629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 10:16:27 +01:00
Benjamin Canac
1d95eb7246 docs(input): add more examples 2024-11-13 16:58:21 +01:00
Benjamin Canac
7cc26d098d fix(App): remove dir prop (#2630) 2024-11-13 16:23:08 +01:00
Guillaume Chau
9241ba1230 fix(FormField): missing conditions to apply container classes (#2631)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-13 15:57:35 +01:00
Daniel Roe
3396d5cebe ci: run vite build to test playground (#2624) 2024-11-13 12:22:46 +01:00
Alex
937585cb3f feat(locale): provide dir on defineLocale (#2620)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-13 12:19:21 +01:00
renovate[bot]
9c00f7c7b7 chore(deps): update all non-major dependencies (v3) (#2626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-13 11:58:02 +01:00
Sandro Circi
73e25ed235 fix(locale): it translation (#2623) 2024-11-12 21:25:25 +01:00
kyyy
75c5e87724 feat(Form): apply transformations (#2550)
Co-authored-by: Romain Hamel <rom.hml@gmail.com>
2024-11-12 16:43:40 +01:00
max
95aa6f68b3 feat(PinInput): implement component (#2570)
Co-authored-by: Max Steinwand <msteinwand@kues.de>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
Co-authored-by: Romain Hamel <rom.hml@gmail.com>
2024-11-12 16:11:06 +01:00
Inesh Bose
f516d7b36d feat(InputMenu/SelectMenu): add create-item prop (#2472)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-12 15:28:18 +01:00
renovate[bot]
300ccc4885 chore(deps): update all non-major dependencies (v3) (#2605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:29:28 +01:00
Alex
e48b416e3b cli: add doc template (#2616)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-12 14:19:32 +01:00
Romain Hamel
17170bb998 playground(form): update examples (#2613) 2024-11-12 13:57:04 +01:00
renovate[bot]
fa5a3752c9 chore(deps): update tailwindcss to v4.0.0-alpha.33 (v3) (#2493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-12 13:56:31 +01:00
Benjamin Canac
fc9711223b chore(github): update issue templates 2024-11-12 13:11:42 +01:00
Alex
8a8b1ee2e1 feat(locale): provide code (#2611) 2024-11-12 12:57:40 +01:00
Benjamin Canac
30218f1b5b feat(NavigationMenu): control items open & defaultOpen on vertical
Resolves #2608
2024-11-12 11:12:19 +01:00
Romain Hamel
3584a3328b fix(Form): match error-pattern on input validation (#2606) 2024-11-11 22:50:22 +01:00
renovate[bot]
6d3dbdbee5 chore(deps): update all non-major dependencies (v3) (#2598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 21:07:54 +01:00
renovate[bot]
c614a0aafc chore(deps): lock file maintenance (v3) (#2596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 19:26:57 +01:00
Sandro Circi
df7a61a97a fix(useLocale): missing import in various components (#2603) 2024-11-11 19:24:33 +01:00
Romain Hamel
143612ec73 feat(FormField): add error-pattern prop (#2601) 2024-11-11 18:35:27 +01:00
Benjamin Canac
18931acdb3 fix(InputMenu/SelectMenu): init filter with labelKey 2024-11-11 00:28:43 +01:00
Benjamin Canac
bbc6bf2455 docs(input-menu/select-menu): add countries picker examples 2024-11-11 00:08:16 +01:00
Benjamin Canac
ff1e0798d3 feat(SelectMenu): use UInput in search to handle props like icon
Resolves #2021
2024-11-10 23:22:44 +01:00
Benjamin Canac
b0be26d67f fix(Toaster): teleport to body
Resolves #2404
2024-11-10 19:21:50 +01:00
Benjamin Canac
36ea3e4045 chore(scripts): remove 2024-11-10 18:36:52 +01:00
Adam Kasper
4889d30b44 feat(locale): add support for Czech translation (#2593) 2024-11-10 18:17:32 +01:00
Benjamin Canac
944a7e0f07 Revert "fix(module): resolve #build/app.config import for vue and nuxt"
This reverts commit d6943e39c0.
2024-11-10 17:25:33 +01:00
Benjamin Canac
d6943e39c0 fix(module): resolve #build/app.config import for vue and nuxt
Resolves #2560
2024-11-10 16:45:46 +01:00
Benjamin Canac
ddb46905e7 fix(App): missing vue imports 2024-11-10 16:44:09 +01:00
renovate[bot]
0e74dbebce chore(deps): update all non-major dependencies (v3) (#2579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 14:45:10 +01:00
Benjamin Canac
9e2cc5b125 fix(Modal/Slideover): prevent esc with prevent-close prop
Resolves #2501
2024-11-10 10:19:47 +01:00
Benjamin Canac
ea97759c2c feat(Popover): add prevent-close prop
Resolves #2245
2024-11-10 10:18:08 +01:00
Dewdew
95a0bbc581 fix(Link): missing relative import (#2588)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-10 10:05:17 +01:00
Benjamin Canac
ecd63ad8d6 test: update vue snapshots 2024-11-10 09:42:11 +01:00
Benjamin Canac
47f58f52ef fix(ContextMenu/DropdownMenu): relative imports with prefix 2024-11-10 09:39:37 +01:00
Benjamin Canac
446f9c1085 feat(Table): add caption prop 2024-11-09 23:55:26 +01:00
Benjamin Canac
7e8a1dd496 chore(readme): update 2024-11-09 22:06:55 +01:00
Benjamin Canac
89ee31b7ae chore(readme): update 2024-11-09 22:03:56 +01:00
Benjamin Canac
95be76940c docs(getting-started): use ::steps and mention css files 2024-11-09 22:03:49 +01:00
Benjamin Canac
761afaf40d docs(deps): update @nuxt/ui-pro 2024-11-09 21:50:20 +01:00
Sandro Circi
d167c9b807 fix(locale): Italian translation (#2584) 2024-11-09 16:15:43 +01:00
Alex
824ba56291 feat(cli): add locale command (#2586) 2024-11-09 16:14:29 +01:00
Sandro Circi
4fbbb25f68 feat(locale): add support for Italian (#2583) 2024-11-09 13:50:03 +01:00
Muhammad Mahmoud
602a667343 feat(locale): add support for Arabic (#2582) 2024-11-09 13:49:50 +01:00
BlackWhite
febda5c2b6 feat(locale): translate chinese (#2580) 2024-11-09 10:40:42 +01:00
Malik-Jouda
20379f51cc docs(nuxt.config): cannot use import.meta outside a module (#2578) 2024-11-09 10:06:56 +01:00
renovate[bot]
1ec56f3326 chore(deps): update all non-major dependencies (v3) (#2572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 18:03:14 +01:00
Alex
1f44d58b64 docs(i18n): auto generated lang support (#2574)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-08 17:48:42 +01:00
Benjamin Canac
5392f988b8 docs(app): fetch files lazy on client 2024-11-08 17:35:42 +01:00
Alex
26362408b1 feat(module): support i18n in components (#2553)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-08 17:22:57 +01:00
renovate[bot]
1e7638bd03 chore(deps): update all non-major dependencies (v3) (#2563)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-08 16:55:39 +01:00
Romain Hamel
afe40033b0 fix(module): skip devtools renderer page injection if router integration is disabled (#2571) 2024-11-08 16:27:25 +01:00
183 changed files with 8752 additions and 2740 deletions

View File

@@ -5,8 +5,8 @@ body:
- type: markdown
attributes:
value: |
> [!IMPORTANT]
> As Nuxt UI v3 is currently in alpha, we recommend thorough testing before using it in production environments. We're actively working on stabilization and welcome feedback from early adopters to improve the library.
> [!IMPORTANT]
> As Nuxt UI v3 is currently in alpha, we recommend thorough testing before using it in production environments. We're actively working on stabilization and welcome feedback from early adopters to improve the library.
- type: markdown
attributes:
value: |
@@ -29,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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
[![nuxt-ui.png](https://repository-images.githubusercontent.com/428329515/43fec891-9030-4601-8233-5d45ba5c6013)](https://ui.nuxt.com)
# Nuxt UI 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

View File

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

View 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
}
})

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

View File

@@ -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
}
})

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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);

View File

@@ -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']"

View File

@@ -126,7 +126,7 @@ const previewUrl = computed(() => {
</div>
<div v-if="highlightedCode && formattedCode" v-show="rendererReady" class="relative w-full p-3">
<!-- eslint-disable vue/no-v-html -->
<pre class="p-4 min-h-40 max-h-72 text-sm overflow-y-auto rounded-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"

View File

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

View File

@@ -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>

View File

@@ -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">

View File

@@ -117,8 +117,8 @@ const optionsValues = ref(props.options?.reduce((acc, option) => {
<template>
<div class="my-5">
<template v-if="preview">
<div class="border border-[var(--ui-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

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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>

View File

@@ -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"

View File

@@ -7,9 +7,9 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.duration"
size="sm"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
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>

View File

@@ -7,9 +7,9 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.expand"
size="sm"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
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>

View File

@@ -10,9 +10,9 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.position"
size="sm"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
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>

View File

@@ -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

View File

@@ -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

View File

@@ -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'
}
```

View File

@@ -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.

View File

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

View 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

View 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

View File

@@ -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

View File

@@ -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`.

View File

@@ -64,7 +64,7 @@ It requires two props:
::
::
Errors are reported directly to the [FormField](/components/form-field) component based on the `name` 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"}.

View File

@@ -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

View 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

View File

@@ -25,15 +25,15 @@ props:
Use the `type` prop to change the input type. Defaults to `text`.
Some types have been implemented in their own components such as [Checkbox](/components/checkbox), [Radio](/components/radio-group), 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.

View File

@@ -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.

View File

@@ -137,7 +137,7 @@ Each item can take a `children` array of objects with the following properties t
Use the `orientation` prop to change the orientation of the NavigationMenu.
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children.
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.

View 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

View File

@@ -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

View File

@@ -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'
],

View File

@@ -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"
}
}

View 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)

View File

@@ -1,8 +1,8 @@
{
"name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "3.0.0-alpha.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"
}

View File

@@ -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"
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -8,7 +8,7 @@ const orientations = Object.keys(theme.variants.orientation)
const color = ref(theme.defaultVariants.color)
const highlightColor = ref()
const variant = ref(theme.defaultVariants.variant)
const orientation = ref('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.',

View 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>

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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)
})

View File

@@ -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

View File

@@ -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

View File

@@ -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({

View File

@@ -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)"

View File

@@ -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 />

View File

@@ -21,7 +21,7 @@ export interface BreadcrumbItem extends Omit<LinkProps, 'raw' | 'custom'> {
export interface BreadcrumbProps<T> {
/**
* The element or component this component should render as.
* @defaultValue '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>

View File

@@ -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)"
/>

View File

@@ -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>

View File

@@ -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>()

View File

@@ -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>()

View File

@@ -38,7 +38,7 @@ extendDevtoolsMeta({ example: 'FormExample' })
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
import { useEventBus } from '@vueuse/core'
import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField'
import { 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>

View File

@@ -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 })">

View File

@@ -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 })" />

View 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>

View File

@@ -73,8 +73,8 @@ export interface LinkProps extends NuxtLinkProps {
active?: boolean
/** Will only be active if the current route is an exact match. */
exact?: boolean
/** 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
}

View File

@@ -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 })"
/>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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 })" />

View File

@@ -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 })" />

View File

@@ -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 })"
/>

View File

@@ -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>

View File

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

View File

@@ -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

View File

@@ -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>

View 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' })
}

View File

@@ -19,7 +19,7 @@ export const formOptionsInjectionKey: InjectionKey<ComputedRef<FormInjectedOptio
export const formBusInjectionKey: InjectionKey<UseEventBusReturn<FormEvent, string>> = Symbol('nuxt-ui.form-events')
export const formFieldInjectionKey: InjectionKey<ComputedRef<FormFieldInjectedOptions<FormFieldProps>>> = Symbol('nuxt-ui.form-field')
export const inputIdInjectionKey: InjectionKey<Ref<string | undefined>> = Symbol('nuxt-ui.input-id')
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, 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