Compare commits

...

81 Commits

Author SHA1 Message Date
Benjamin Canac
52958af81a chore(release): 2.8.0 2023-09-07 15:17:32 +02:00
Benjamin Canac
de4416d5bf chore(release-it): preset name 2023-09-07 15:17:19 +02:00
Sébastien Chopin
9ae038489e docs: use new url for module stats 2023-09-07 15:13:48 +02:00
Benjamin Canac
6ad1afd308 docs: update badges 2023-09-07 15:13:48 +02:00
SevicheCC
ab5153ac19 feat(Form): add valibot supprt (#615)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-07 15:13:48 +02:00
Benjamin Canac
eebff72d01 docs: bump @nuxthq/elements 2023-09-07 15:13:48 +02:00
Benjamin Canac
c20aefdd91 chore(github): put back typecheck as @nuxthq/elements uses @nuxt/ui-edge 2023-09-07 15:13:48 +02:00
Benjamin Canac
0f252d0caf fix(module): missing useHead import 2023-09-07 15:13:48 +02:00
Benjamin Canac
888effea0a fix(module): missing useNuxtApp import 2023-09-07 15:13:48 +02:00
Benjamin Canac
3ed282df98 chore(github): disable typecheck to publish edge package 2023-09-07 15:13:48 +02:00
Benjamin Canac
22f7536154 chore: migrate to https://ui.nuxt.com and @nuxt/ui (#616) 2023-09-07 15:13:48 +02:00
Benjamin Canac
9f9d8f5cec docs: landing page (#611)
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
2023-09-07 15:13:48 +02:00
Benjamin Canac
190378aaa9 chore(Alert): optional click function 2023-09-07 15:13:48 +02:00
Benjamin Canac
9b3a22ea14 fix(Radio): put back id for label selection 2023-09-07 15:13:48 +02:00
Benjamin Canac
7c157ce886 fix(Badge): allow label as number 2023-09-07 15:13:47 +02:00
Benjamin Canac
e49c673573 fix(AvatarGroup): pass default size to max avatar 2023-09-07 15:13:47 +02:00
Benjamin Canac
e578b0dd9e fix(AvatarGroup): add justify-end to wrapper to prevent right align 2023-09-07 15:13:47 +02:00
Eduard Aymerich
b3bc6e2e9e feat(ButtonGroup): add orientation prop (#603)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-07 15:13:47 +02:00
Kekeocha Justin Chetachukwu
e04c212d0d chore(Table): add overflow-x-auto to wrapper (#609) 2023-09-07 15:13:47 +02:00
renovate[bot]
92da3238eb chore(deps): update all non-major dependencies (#593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-07 15:13:47 +02:00
Aditio Pangestu
d9363168b2 fix(Table): missing component imports (#608) 2023-09-07 15:13:47 +02:00
Benjamin Canac
53b2655ae5 docs: improve props types (#588) 2023-09-07 15:13:47 +02:00
Sébastien Chopin
f12c149e4e docs: remove concurrency to 1 2023-09-07 15:13:47 +02:00
Benjamin Canac
c4bcf0220b chore(deps): pin @nuxtjs/mdc 2023-09-07 15:13:47 +02:00
Benjamin Canac
73fc310e8d chore(deps): bump 2023-09-07 15:13:47 +02:00
Sébastien Chopin
7dff23912d docs: improve performances (#570)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-07 15:13:47 +02:00
Benjamin Canac
1145f88bca docs(deps): bump @nuxthq/elements 2023-09-07 15:13:47 +02:00
Sébastien Chopin
573c8a2f54 docs: fix social card link 2023-09-07 15:13:47 +02:00
Sébastien Chopin
7dbfe4ecd6 docs: update readme 2023-09-07 15:13:47 +02:00
Benjamin Canac
98b3c3550c docs: add missing component slots 2023-09-07 15:13:47 +02:00
Sébastien Chopin
791804b2fb fix: use head instance from plugin 2023-09-07 15:13:47 +02:00
Ling
f1ed0076e5 fix(Tooltip): hide on touch devices (#580) 2023-09-07 15:13:47 +02:00
Eduard Aymerich
11980a3c9c docs(ComponentCard): prevent label prop as select (#568) 2023-09-07 15:13:47 +02:00
Benjamin Canac
b901222c4b docs: specify multi-word component titles 2023-09-07 15:13:47 +02:00
Benjamin Canac
2e056fa3cf docs(SelectMenu): add slots examples
Resolves #557
2023-09-07 15:13:47 +02:00
renovate[bot]
b955f57084 chore(deps): update devdependency unbuild to v2 (#565)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-07 15:13:47 +02:00
rogepi
1435856586 docs: add colon before numeric props (#564)
Co-authored-by: rogepi <rogepi@outlook.com>
2023-09-07 15:13:47 +02:00
adjabaev
ce160c9a97 docs: dead links in Form and FormGroup pages (#544) 2023-09-07 15:13:47 +02:00
renovate[bot]
c88c8094a5 chore(deps): update all non-major dependencies (#530)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-07 15:13:47 +02:00
Benjamin Canac
7e7e9d0f85 docs: add version select (#532) 2023-09-07 15:13:47 +02:00
Benjamin Canac
ee663157b7 chore(Table): handle loading-state prop merge like empty-state 2023-09-07 15:13:47 +02:00
Eduard Aymerich
44ba758c0d fix(Table): empty state is displayed if null (#517) 2023-09-07 15:13:47 +02:00
Benjamin Canac
cb5484a603 docs: bump @nuxthq/elements 2023-09-07 15:13:47 +02:00
Benjamin Canac
998314e1cb fix(SelectMenu): invalid gap values 2023-09-07 15:13:47 +02:00
Benjamin Canac
d4e3ab606b fix(ButtonGroup): switch back to ui prop 2023-09-07 15:13:47 +02:00
Benjamin Canac
0a7c50ba98 chore(Popover): set default open-delay to 0 2023-09-07 15:13:47 +02:00
Benjamin Canac
88cc2e93af chore(Dropdown): set default open-delay to 0 2023-09-07 15:13:47 +02:00
Benjamin Canac
39042b3de1 fix(FormGroup): add missing ref import from vue 2023-09-07 15:13:47 +02:00
Benjamin Canac
8880bdc456 feat(module)!: use tailwind-merge for class merging (#509) 2023-09-07 15:13:47 +02:00
Romain Hamel
6d7973f6e1 feat(Form): improve form control and input validation trigger (#487)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-07 15:13:47 +02:00
renovate[bot]
60bb74675c chore(deps): update all non-major dependencies (#340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-07 15:13:47 +02:00
Haytham A. Salama
a488b879f5 chore: add eslint rules for spacing (#526) 2023-09-07 15:13:47 +02:00
Benjamin Canac
fa1103b4ec docs: rebrand to Nuxt UI 2023-09-07 15:13:47 +02:00
Benjamin Canac
28ebfc2575 docs: @nuxt-themes/ui-kit is now @nuxthq/elements 2023-09-07 15:13:47 +02:00
Christian López C
fdce429b3e fix(Tabs): recompute marker position when v-model changes (#524) 2023-09-07 15:13:47 +02:00
Eduard Aymerich
7e2bebd3ef feat(Modal): add fullscreen prop (#523)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-07 15:13:47 +02:00
Benjamin Canac
ccb0f6207c chore(Table): typecheck 2023-09-07 15:13:47 +02:00
Benjamin Canac
f501460ebb docs: disable @nuxt/devtools 2023-09-07 15:13:47 +02:00
Vladyslav
858886a852 feat(Table): support nested keys in columns (#503)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-07 15:13:47 +02:00
Benjamin Canac
74f4903836 docs: bump @nuxt-themes/ui-kit 2023-09-07 15:13:47 +02:00
Benjamin Canac
16ac4a0533 docs: bump @nuxt-themes/ui-kit 2023-09-07 15:13:47 +02:00
Benjamin Canac
451e72a583 chore(app.config): revert -primary shortcuts after #493
Class priority issues in some cases when ring already defined on dark mode for example (input).
2023-09-07 15:13:47 +02:00
171H
a8a1c150a0 fix(Button): add missing prop types (#508) 2023-09-07 15:13:47 +02:00
171H
b243e8c946 fix(Alert): fix wrong type of actions (#507) 2023-09-07 15:13:47 +02:00
Benjamin Canac
a29877059e docs: improve icon sections of Alert, Avatar and Notification 2023-09-07 15:13:47 +02:00
Benjamin Canac
55daed0e5a feat(Avatar): handle icon default from app.config.ts 2023-09-07 15:13:47 +02:00
Benjamin Canac
1c00a366c2 chore(Link): use $route instead of useRoute() 2023-09-07 15:13:47 +02:00
Benjamin Canac
9866f051b2 chore(Avatar): add flex-shrink-0 to wrapper 2023-09-07 15:13:47 +02:00
Paul Grau
3d6839da97 fix(Form): fix wrong type of validate (#496)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-07 15:13:47 +02:00
Paul Grau
8b19b1880e fix(Form): use safeParseAsync for zod (#497) 2023-09-07 15:13:47 +02:00
Benjamin Canac
2d6badd4b0 docs(Avatar): add edge badge on icon 2023-09-07 15:13:47 +02:00
Benjamin Canac
df3b2028ed feat(Avatar): add icon prop as fallback 2023-09-07 15:13:47 +02:00
Benjamin Canac
eb609b13e4 fix(AvatarGroup): use ui.wrapper as inheritAttrs is not false 2023-09-07 15:13:47 +02:00
Benjamin Canac
aaf09ad555 feat(Tabs): control selected index (#490) 2023-09-07 15:13:47 +02:00
Benjamin Canac
ad0fe230ba docs(ui): also add the --color-primary-DEFAULT variable 2023-09-07 15:13:47 +02:00
Benjamin Canac
c6056ed133 feat(module): add DEFAULT shade to primary color (#493) 2023-09-07 15:13:47 +02:00
Benjamin Canac
7008df0988 fix(FormGroup): size were invalid since default has been removed
Bug introduced in c59595f2c6
2023-09-07 15:13:47 +02:00
Benjamin Canac
dc951ff69d fix(Popover): handle hover mode with padding like dropdown 2023-09-07 15:13:47 +02:00
Romain Hamel
e2146a5a58 docs(Form): fixed invalid state attributes in examples (#479) 2023-09-07 15:13:47 +02:00
Benjamin Canac
75d26e0c2b docs: improve dynamic page 2023-09-07 15:13:47 +02:00
Benjamin Canac
32a32d00ab docs: update badges 2023-08-12 22:26:02 +02:00
177 changed files with 8718 additions and 3911 deletions

View File

@@ -14,6 +14,7 @@ module.exports = {
'key-spacing': ['error', { beforeColon: false, afterColon: true, mode: 'strict' }],
'space-before-blocks': ['error', 'always'],
'space-infix-ops': ['error', { int32Hint: false }],
'no-multi-spaces': ['error', { ignoreEOLComments: true }],
// Typescript
'@typescript-eslint/type-annotation-spacing': 'error',
@@ -21,6 +22,7 @@ module.exports = {
// Vuejs
'vue/multi-word-component-names': 0,
'vue/html-indent': ['error', 2],
'vue/comma-spacing': ['error', { before: false, after: true }],
'vue/script-indent': ['error', 2, { baseIndent: 0 }],
'vue/keyword-spacing': ['error', { before: true, after: true }],
'vue/object-curly-spacing': ['error', 'always'],

View File

@@ -11,7 +11,7 @@ assignees: ''
Before reporting a bug, please make sure that you have read through our documentation and you think your problem is indeed an issue related to our module. -->
### Version
@nuxthq/ui: <!-- ex: v2.0.0 -->
@nuxt/ui: <!-- ex: v2.0.0 -->
nuxt: <!-- ex: v3.5.0 -->
### Reproduction Link
@@ -19,7 +19,7 @@ nuxt: <!-- ex: v3.5.0 -->
<!--
A minimal test case based on one of:
- a GitHub repository that can reproduce the bug
- https://stackblitz.com/edit/nuxtlabs-ui
- https://stackblitz.com/edit/nuxt-ui
-->
### Steps to reproduce

View File

@@ -14,7 +14,9 @@
},
"plugins": {
"@release-it/conventional-changelog": {
"preset": "conventionalcommits",
"preset": {
"name": "conventionalcommits"
},
"infile": "CHANGELOG.md",
"header": "# Changelog",
"ignoreRecommendedBump": true

View File

@@ -1,5 +1,50 @@
# Changelog
## [2.8.0](https://github.com/nuxt/ui/compare/v2.7.0...v2.8.0) (2023-09-07)
### ⚠ BREAKING CHANGES
* **module:** use `tailwind-merge` for class merging (#509)
### Features
* **Avatar:** add `icon` prop as fallback ([df3b202](https://github.com/nuxt/ui/commit/df3b2028ed2a68178c41e136985500bc0e6f4608))
* **Avatar:** handle `icon` default from `app.config.ts` ([55daed0](https://github.com/nuxt/ui/commit/55daed0e5a220cc5f2abf1bf4a27bc86773b7939))
* **ButtonGroup:** add `orientation` prop ([#603](https://github.com/nuxt/ui/issues/603)) ([b3bc6e2](https://github.com/nuxt/ui/commit/b3bc6e2e9e9446ee3dab7ae02f939a9c01c4218b))
* **Form:** add valibot supprt ([#615](https://github.com/nuxt/ui/issues/615)) ([ab5153a](https://github.com/nuxt/ui/commit/ab5153ac19703c11b107825208e33d04e01a9be2))
* **Form:** improve form control and input validation trigger ([#487](https://github.com/nuxt/ui/issues/487)) ([6d7973f](https://github.com/nuxt/ui/commit/6d7973f6e1cc3552df45ac7ce2e2107d18061abf))
* **Modal:** add `fullscreen` prop ([#523](https://github.com/nuxt/ui/issues/523)) ([7e2bebd](https://github.com/nuxt/ui/commit/7e2bebd3ef88ea65a0dd03686e6a9d08b0d1edd7))
* **module:** add `DEFAULT` shade to `primary` color ([#493](https://github.com/nuxt/ui/issues/493)) ([c6056ed](https://github.com/nuxt/ui/commit/c6056ed13323f854a9ab4a07303b9e64cd0f563a))
* **module:** use `tailwind-merge` for class merging ([#509](https://github.com/nuxt/ui/issues/509)) ([8880bdc](https://github.com/nuxt/ui/commit/8880bdc45640103aea3e84a5410567bd87607738))
* **Table:** support nested keys in columns ([#503](https://github.com/nuxt/ui/issues/503)) ([858886a](https://github.com/nuxt/ui/commit/858886a85288370bfc7c0991e96929811f20f561))
* **Tabs:** control selected index ([#490](https://github.com/nuxt/ui/issues/490)) ([aaf09ad](https://github.com/nuxt/ui/commit/aaf09ad555f713738958b191e5649dc80bd3ba96))
### Bug Fixes
* **Alert:** fix wrong type of `actions` ([#507](https://github.com/nuxt/ui/issues/507)) ([b243e8c](https://github.com/nuxt/ui/commit/b243e8c94649a50358a5961d45b5f48c6c670383))
* **AvatarGroup:** add `justify-end` to wrapper to prevent right align ([e578b0d](https://github.com/nuxt/ui/commit/e578b0dd9e924cacf22ed541e4da54e558654254))
* **AvatarGroup:** pass default `size` to max avatar ([e49c673](https://github.com/nuxt/ui/commit/e49c67357364483d742bf9ccc7a94dc67edafe96))
* **AvatarGroup:** use `ui.wrapper` as `inheritAttrs` is not false ([eb609b1](https://github.com/nuxt/ui/commit/eb609b13e47da3e171351884f7fe6e7dcaa5ed77))
* **Badge:** allow `label` as number ([7c157ce](https://github.com/nuxt/ui/commit/7c157ce886fd6f35886255a5a2ee468c2b2089c3))
* **Button:** add missing prop types ([#508](https://github.com/nuxt/ui/issues/508)) ([a8a1c15](https://github.com/nuxt/ui/commit/a8a1c150a00212eeb8cde32ff06ee3b6c45fbedd))
* **ButtonGroup:** switch back to `ui` prop ([d4e3ab6](https://github.com/nuxt/ui/commit/d4e3ab606b19337c33e541ae399466ba8551e898))
* **Form:** fix wrong type of validate ([#496](https://github.com/nuxt/ui/issues/496)) ([3d6839d](https://github.com/nuxt/ui/commit/3d6839da97a09747b0090a3d5aa1ebe3d28b85fd))
* **FormGroup:** `size` were invalid since default has been removed ([7008df0](https://github.com/nuxt/ui/commit/7008df098887965f2ed3e43d2ccb64b3d32524b9))
* **FormGroup:** add missing `ref` import from vue ([39042b3](https://github.com/nuxt/ui/commit/39042b3de17efc26ee86b003a05e42e7a41f50bf))
* **Form:** use safeParseAsync for zod ([#497](https://github.com/nuxt/ui/issues/497)) ([8b19b18](https://github.com/nuxt/ui/commit/8b19b1880e744d81481b4e1f5b4e88d7f48f7bdb))
* **module:** missing `useHead` import ([0f252d0](https://github.com/nuxt/ui/commit/0f252d0caf550ba7ea4cfb24bed5f95a42e78970))
* **module:** missing `useNuxtApp` import ([888effe](https://github.com/nuxt/ui/commit/888effea0a66f5dd88cdd47d5d65da02bdec6ad6))
* **Popover:** handle `hover` mode with padding like dropdown ([dc951ff](https://github.com/nuxt/ui/commit/dc951ff69dd15dc942e5c61edb6bc0a5a516c89b))
* **Radio:** put back `id` for label selection ([9b3a22e](https://github.com/nuxt/ui/commit/9b3a22ea140e5a70c288c7b6241671e9d3542572))
* **SelectMenu:** invalid `gap` values ([998314e](https://github.com/nuxt/ui/commit/998314e1cbafced2844b06eac5f34fa3ddb8d1e5))
* **Table:** empty state is displayed if null ([#517](https://github.com/nuxt/ui/issues/517)) ([44ba758](https://github.com/nuxt/ui/commit/44ba758c0d50f2214554a477d661a914df2025ba))
* **Table:** missing component imports ([#608](https://github.com/nuxt/ui/issues/608)) ([d936316](https://github.com/nuxt/ui/commit/d9363168b282acc332a473340b77ee8f702e0e3f))
* **Tabs:** recompute marker position when `v-model` changes ([#524](https://github.com/nuxt/ui/issues/524)) ([fdce429](https://github.com/nuxt/ui/commit/fdce429b3ef1d203b2438299e46e57a010355fb0))
* **Tooltip:** hide on touch devices ([#580](https://github.com/nuxt/ui/issues/580)) ([f1ed007](https://github.com/nuxt/ui/commit/f1ed0076e5ada78ba6150745ce9d8459cc731b4e))
* use head instance from plugin ([791804b](https://github.com/nuxt/ui/commit/791804b2fba6493f07dc75b09f8b8ac95f71644d))
## [2.7.0](https://github.com/nuxtlabs/ui/compare/v2.6.0...v2.7.0) (2023-08-01)

View File

@@ -1,13 +1,15 @@
# NuxtLabs UI
[![nuxt-ui-social-card](https://repository-images.githubusercontent.com/428329515/43fec891-9030-4601-8233-5d45ba5c6013)](https://ui.nuxt.com)
# Nuxt UI
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![License][license-src]][license-href]
[![Nuxt][nuxt-src]][nuxt-href]
This module has been developed by [NuxtLabs](https://nuxtlabs.com/) for [Volta](https://volta.net) and [Nuxt Studio](https://nuxt.studio/). It provides everything related to UI when building a Nuxt application, including components, icons, colors, dark mode and also keyboard shortcuts.
Nuxt UI provides everything related to UI when building Nuxt applications: components, icons, colors, dark mode and also keyboard shortcuts.
[![social preview](https://repository-images.githubusercontent.com/428329515/5a18c5dd-bb58-4874-b6ef-1c44e4884344)](https://ui.nuxtlabs.com)
Is has been developed by [NuxtLabs](https://nuxtlabs.com/) for [Volta](https://volta.net), [Nuxt Studio](https://nuxt.studio/) and the Nuxt community.
## Features
@@ -19,10 +21,19 @@ This module has been developed by [NuxtLabs](https://nuxtlabs.com/) for [Volta](
- Bundled icons
- Fully typed
Read more on [ui.nuxt.com](https://ui.nuxt.com)
## Installation
```bash
yarn add --dev @nuxthq/ui
# Using npm
npm install @nuxt/ui
# Using yarn
yarn add @nuxt/ui
# Using pnpm
pnpm add @nuxt/ui
```
Then, register the module in your `nuxt.config.ts`:
@@ -30,24 +41,24 @@ Then, register the module in your `nuxt.config.ts`:
```js
export default defineNuxtConfig({
modules: [
'@nuxthq/ui'
'@nuxt/ui'
]
})
```
If you want latest updates, please use `@nuxthq/ui-edge` in your `package.json`:
If you want latest updates, please use `@nuxt/ui-edge` in your `package.json`:
```json
{
"devDependencies": {
"@nuxthq/ui": "npm:@nuxthq/ui-edge@latest"
"@nuxt/ui": "npm:@nuxt/ui-edge@latest"
}
}
```
## Documentation
Visit https://ui.nuxtlabs.com to view the documentation.
Visit https://ui.nuxt.com to explore the documentation.
## Credits
@@ -61,17 +72,17 @@ Visit https://ui.nuxtlabs.com to view the documentation.
## License
Licensed under the [MIT license](https://github.com/nuxtlabs/ui/blob/dev/LICENSE.md).
Licensed under the [MIT license](https://github.com/nuxt/ui/blob/dev/LICENSE.md).
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/@nuxthq/ui/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/@nuxthq/ui
[npm-version-src]: https://img.shields.io/npm/v/@nuxt/ui/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/@nuxt/ui
[npm-downloads-src]: https://img.shields.io/npm/dm/@nuxthq/ui.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-downloads-href]: https://npmjs.com/package/@nuxthq/ui
[npm-downloads-src]: https://img.shields.io/npm/dm/@nuxt/ui.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-downloads-href]: https://npmjs.com/package/@nuxt/ui
[license-src]: https://img.shields.io/github/license/nuxtlabs/ui.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://github.com/nuxtlabs/ui/blob/main/LICENSE
[license-src]: https://img.shields.io/github/license/nuxt/ui.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://github.com/nuxt/ui/blob/main/LICENSE
[nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
[nuxt-href]: https://nuxt.com

View File

@@ -1 +1,4 @@
NUXT_UI_KIT_TOKEN=
# To use Nuxt Elements in production
NUXT_ELEMENTS_TOKEN=
# Used when pre-rendering the docs for dynamic OG images
NUXT_PUBLIC_SITE_URL=

6
docs/app.config.ts Normal file
View File

@@ -0,0 +1,6 @@
export default defineAppConfig({
ui: {
primary: 'green',
gray: 'slate'
}
})

View File

@@ -1,48 +1,16 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div>
<UHeader>
<template #left>
<NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
<Header />
<span class="hidden sm:block">NuxtLabs</span><span class="sm:text-primary-500 dark:sm:text-primary-400">UI</span>
</NuxtLink>
</template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<template #center>
<UDocsSearchButton class="ml-1.5 flg:w-64 xl:w-96" />
</template>
<template #right>
<ColorPicker />
<UColorModeButton />
<USocialButton to="https://twitter.com/nuxtlabs" target="_blank" icon="i-simple-icons-twitter" class="hidden lg:inline-flex" />
<USocialButton to="https://github.com/nuxtlabs/ui" target="_blank" icon="i-simple-icons-github" class="hidden lg:inline-flex" />
</template>
<template #links>
<UAsideAnchors :links="anchors" />
<UAsideLinks :links="links" />
</template>
</UHeader>
<UMain>
<UContainer>
<UPage>
<template #left>
<UAside :links="links" :anchors="anchors" />
</template>
<NuxtPage />
</UPage>
</UContainer>
</UMain>
<Footer />
<ClientOnly>
<UDocsSearch :files="files" :links="links" />
<LazyUDocsSearch :files="files" :navigation="navigation" />
</ClientOnly>
<UNotifications>
@@ -58,55 +26,54 @@
</template>
<script setup lang="ts">
import { withoutTrailingSlash } from 'ufo'
const route = useRoute()
const colorMode = useColorMode()
const { prefix, removePrefixFromNavigation, removePrefixFromFiles } = useContentSource()
const { data: links } = await useAsyncData('navigation', () => fetchContentNavigation(), {
transform: (navigation) => mapContentLinks(navigation)
const { data: nav } = await useAsyncData('navigation', () => fetchContentNavigation())
const { data: search } = useLazyFetch('/api/search.json', {
default: () => [],
server: false
})
const { data: files } = await useLazyAsyncData('files', () => queryContent().where({ _type: 'markdown', navigation: { $ne: false } }).find(), { default: () => [] })
provide('links', links)
const anchors = [{
label: 'Documentation',
icon: 'i-heroicons-book-open-solid',
to: '/getting-started'
}, {
label: 'Playground',
icon: 'i-simple-icons-stackblitz',
to: 'https://stackblitz.com/edit/nuxtlabs-ui?file=app.config.ts,app.vue',
target: '_blank'
}, {
label: 'Releases',
icon: 'i-heroicons-rocket-launch-solid',
to: 'https://github.com/nuxtlabs/ui/releases',
target: '_blank'
}]
// Computed
const navigation = computed(() => {
const navigation = nav.value.find(link => link._path === prefix.value)?.children || []
return prefix.value === '/main' ? removePrefixFromNavigation(navigation) : navigation
})
const files = computed(() => {
const files = search.value.filter(file => file._path.startsWith(prefix.value))
return prefix.value === '/main' ? removePrefixFromFiles(files) : files
})
const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white')
// Head
useHead({
titleTemplate: title => title && title.includes('NuxtLabs UI') ? title : `${title} - NuxtLabs UI`,
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' },
{ key: 'theme-color', name: 'theme-color', content: color }
],
link: [
{ rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' }
{ rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' },
{ rel: 'canonical', href: `https://ui.nuxt.com${withoutTrailingSlash(route.path)}` }
],
htmlAttrs: {
lang: 'en'
}
})
useSeoMeta({
ogImage: '/social-preview.jpg',
twitterImage: '/social-preview.jpg',
useServerSeoMeta({
ogSiteName: 'Nuxt UI',
twitterCard: 'summary_large_image'
})
// Provide
provide('navigation', navigation)
provide('files', files)
</script>

View File

@@ -0,0 +1,50 @@
<template>
<div class="mb-3 lg:mb-6">
<label for="branch" class="block mb-1.5 font-semibold text-sm/6">Version</label>
<USelectMenu
id="branch"
:model-value="branch"
name="branch"
:options="branches"
color="gray"
:ui="{ icon: { trailing: { padding: { sm: 'pe-1.5' } } } }"
:ui-menu="{ option: { container: 'gap-1.5' } }"
@update:model-value="selectBranch"
>
<template #label>
<UIcon v-if="branch.icon" :name="branch.icon" class="w-4 h-4 flex-shrink-0 text-gray-600 dark:text-gray-300" />
<span class="font-medium">{{ branch.label }}</span>
<span class="truncate text-gray-400 dark:text-gray-500">{{ branch.suffix }}</span>
</template>
<template #option="{ option }">
<UIcon v-if="option.icon" :name="option.icon" class="w-4 h-4 flex-shrink-0 text-gray-600 dark:text-gray-300" />
<span class="font-medium">{{ option.label }}</span>
<span class="truncate text-gray-400 dark:text-gray-500">{{ option.suffix }}</span>
</template>
</USelectMenu>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const router = useRouter()
const { branches, branch } = useContentSource()
function selectBranch (branch) {
if (branch.name === 'dev') {
if (route.path.startsWith('/dev')) {
return
}
router.push(`/dev${route.path}`)
} else {
router.push(route.path.replace('/dev', ''))
}
}
</script>

View File

@@ -1,23 +1,32 @@
<template>
<footer class="flex items-center gap-1.5 mt-12">
<div class="flex-1 flex items-baseline gap-1.5 text-sm text-gray-600 dark:text-gray-300 leading-6">
Made by
<NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs">
<LogoLabs class="text-gray-900 dark:text-white w-14 h-auto" />
</NuxtLink>
<div class="w-full h-px bg-gray-200 dark:bg-gray-800 flex items-center justify-center">
<div class="bg-white dark:bg-gray-900 px-4">
<LogoOnly class="w-5 h-5" />
</div>
</div>
<NuxtLink :to="`https://github.com/nuxtlabs/ui/releases/tag/v${config.version}`" target="_blank" class="inline-flex">
<UBadge :label="`v${config.version}`" />
</NuxtLink>
<UFooter :links="[]" :ui="{ bottom: { container: 'lg:py-4' } }">
<template #left>
<div class="text-sm text-gray-600 dark:text-gray-300">
Made by
<NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs" class="inline-block">
<LogoLabs class="text-gray-900 dark:text-white h-4 w-auto" />
</NuxtLink>
</div>
</template>
<div class="flex-1 flex items-center justify-end gap-1.5 -my-1 lg:hidden">
<USocialButton to="https://twitter.com/nuxtlabs" target="_blank" icon="i-simple-icons-twitter" />
<USocialButton to="https://github.com/nuxtlabs/ui" target="_blank" icon="i-simple-icons-github" />
</div>
</footer>
<template #center>
<span class="text-sm text-gray-600 dark:text-gray-300">
Published under <NuxtLink to="https://github.com/nuxt/ui" target="_blank" class="text-gray-900 dark:text-white">
MIT License
</NuxtLink>
</span>
</template>
<template #right>
<USocialButton aria-label="Nuxt Website" icon="i-simple-icons-nuxtdotjs" to="https://nuxt.com" />
<USocialButton aria-label="Nuxt on X" icon="i-simple-icons-x" to="https://x.com/nuxtlabs" />
<USocialButton aria-label="Nuxt UI on GitHub" icon="i-simple-icons-github" to="https://github.com/nuxt/ui" />
</template>
</UFooter>
</template>
<script setup lang="ts">
const config = useRuntimeConfig().public
</script>

View File

@@ -0,0 +1,66 @@
<template>
<UHeader
:links="links"
:class="{
'border-primary-200/75 dark:border-primary-900/50': $route.path === '/',
'border-gray-200 dark:border-gray-800': $route.path !== '/'
}"
>
<template #left>
<NuxtLink to="/" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-auto h-6" />
</NuxtLink>
</template>
<template v-if="$route.path !== '/'" #center>
<UDocsSearchButton class="ml-1.5 flg:w-64 xl:w-96" />
</template>
<template #right>
<ColorPicker />
<UDocsSearchButton v-if="$route.path === '/'" icon-only />
<UColorModeButton v-if="!$colorMode.forced" />
<USocialButton to="https://github.com/nuxt/ui" target="_blank" icon="i-simple-icons-github" class="hidden lg:inline-flex" />
</template>
<template #panel>
<BranchSelect />
<UNavigationTree :links="mapContentNavigation(navigation)" />
</template>
</UHeader>
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
const route = useRoute()
const { mapContentNavigation } = useElementsHelpers()
const navigation = inject<Ref<NavItem[]>>('navigation')
const links = computed(() => {
if (route.path !== '/') {
return []
}
return [{
label: 'Documentation',
icon: 'i-heroicons-book-open-solid',
to: '/getting-started'
}, {
label: 'Playground',
icon: 'i-simple-icons-stackblitz',
to: 'https://stackblitz.com/edit/nuxt-ui?file=app.config.ts,app.vue',
target: '_blank'
}, {
label: 'Releases',
icon: 'i-heroicons-rocket-launch-solid',
to: 'https://github.com/nuxt/ui/releases',
target: '_blank'
}]
})
</script>

View File

@@ -1,5 +1,11 @@
<template>
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="currentColor" />
<svg width="1020" height="200" viewBox="0 0 1020 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M377 200C379.16 200 381 198.209 381 196V103C381 103 386 112 395 127L434 194C435.785 197.74 439.744 200 443 200H470V50H443C441.202 50 439 51.4941 439 54V148L421 116L385 55C383.248 51.8912 379.479 50 376 50H350V200H377Z" fill="currentColor" />
<path d="M726 92H739C742.314 92 745 89.3137 745 86V60H773V92H800V116H773V159C773 169.5 778.057 174 787 174H800V200H783C759.948 200 745 185.071 745 160V116H726V92Z" fill="currentColor" />
<path d="M591 92V154C591 168.004 585.742 179.809 578 188C570.258 196.191 559.566 200 545 200C530.434 200 518.742 196.191 511 188C503.389 179.809 498 168.004 498 154V92H514C517.412 92 520.769 92.622 523 95C525.231 97.2459 526 98.5652 526 102V154C526 162.059 526.457 167.037 530 171C533.543 174.831 537.914 176 545 176C552.217 176 555.457 174.831 559 171C562.543 167.037 563 162.059 563 154V102C563 98.5652 563.769 96.378 566 94C567.96 91.9107 570.028 91.9599 573 92C573.411 92.0055 574.586 92 575 92H591Z" fill="currentColor" />
<path d="M676 144L710 92H684C680.723 92 677.812 93.1758 676 96L660 120L645 97C643.188 94.1758 639.277 92 636 92H611L645 143L608 200H634C637.25 200 640.182 196.787 642 194L660 167L679 195C680.818 197.787 683.75 200 687 200H713L676 144Z" fill="currentColor" />
<path d="M168 200H279C282.542 200 285.932 198.756 289 197C292.068 195.244 295.23 193.041 297 190C298.77 186.959 300.002 183.51 300 179.999C299.998 176.488 298.773 173.04 297 170.001L222 41C220.23 37.96 218.067 35.7552 215 34C211.933 32.2448 207.542 31 204 31C200.458 31 197.067 32.2448 194 34C190.933 35.7552 188.77 37.96 187 41L168 74L130 9.99764C128.228 6.95784 126.068 3.75491 123 2C119.932 0.245087 116.542 0 113 0C109.458 0 106.068 0.245087 103 2C99.9323 3.75491 96.7717 6.95784 95 9.99764L2 170.001C0.226979 173.04 0.00154312 176.488 1.90993e-06 179.999C-0.0015393 183.51 0.229648 186.959 2 190C3.77035 193.04 6.93245 195.244 10 197C13.0675 198.756 16.4578 200 20 200H90C117.737 200 137.925 187.558 152 164L186 105L204 74L259 168H186L168 200ZM89 168H40L113 42L150 105L125.491 147.725C116.144 163.01 105.488 168 89 168Z" fill="rgb(var(--color-primary-DEFAULT))" />
<path d="M958 60.0001H938C933.524 60.0001 929.926 59.9395 927 63C924.074 65.8905 925 67.5792 925 72V141C925 151.372 923.648 156.899 919 162C914.352 166.931 908.468 169 899 169C889.705 169 882.648 166.931 878 162C873.352 156.899 873 151.372 873 141V72.0001C873 67.5793 872.926 65.8906 870 63.0001C867.074 59.9396 863.476 60.0001 859 60.0001H840V141C840 159.023 845.016 173.458 855 184C865.156 194.542 879.893 200 899 200C918.107 200 932.844 194.542 943 184C953.156 173.458 958 159.023 958 141V60.0001Z" fill="rgb(var(--color-primary-DEFAULT))" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M1000 60.0233L1020 60V77L1020 128V156.007L1020 181L1020 189.004C1020 192.938 1019.98 194.429 1017 197.001C1014.02 199.725 1009.56 200 1005 200H986.001V181.006L986 130.012V70.0215C986 66.1576 986.016 64.5494 989 62.023C991.819 59.6358 995.437 60.0233 1000 60.0233Z" fill="rgb(var(--color-primary-DEFAULT))" />
</svg>
</template>

View File

@@ -0,0 +1,11 @@
<template>
<svg width="1020" height="200" viewBox="0 0 1020 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M377 200C379.16 200 381 198.209 381 196V103C381 103 386 112 395 127L434 194C435.785 197.74 439.744 200 443 200H470V50H443C441.202 50 439 51.4941 439 54V148L421 116L385 55C383.248 51.8912 379.479 50 376 50H350V200H377Z" fill="currentColor" />
<path d="M726 92H739C742.314 92 745 89.3137 745 86V60H773V92H800V116H773V159C773 169.5 778.057 174 787 174H800V200H783C759.948 200 745 185.071 745 160V116H726V92Z" fill="currentColor" />
<path d="M591 92V154C591 168.004 585.742 179.809 578 188C570.258 196.191 559.566 200 545 200C530.434 200 518.742 196.191 511 188C503.389 179.809 498 168.004 498 154V92H514C517.412 92 520.769 92.622 523 95C525.231 97.2459 526 98.5652 526 102V154C526 162.059 526.457 167.037 530 171C533.543 174.831 537.914 176 545 176C552.217 176 555.457 174.831 559 171C562.543 167.037 563 162.059 563 154V102C563 98.5652 563.769 96.378 566 94C567.96 91.9107 570.028 91.9599 573 92C573.411 92.0055 574.586 92 575 92H591Z" fill="currentColor" />
<path d="M676 144L710 92H684C680.723 92 677.812 93.1758 676 96L660 120L645 97C643.188 94.1758 639.277 92 636 92H611L645 143L608 200H634C637.25 200 640.182 196.787 642 194L660 167L679 195C680.818 197.787 683.75 200 687 200H713L676 144Z" fill="currentColor" />
<path d="M168 200H279C282.542 200 285.932 198.756 289 197C292.068 195.244 295.23 193.041 297 190C298.77 186.959 300.002 183.51 300 179.999C299.998 176.488 298.773 173.04 297 170.001L222 41C220.23 37.96 218.067 35.7552 215 34C211.933 32.2448 207.542 31 204 31C200.458 31 197.067 32.2448 194 34C190.933 35.7552 188.77 37.96 187 41L168 74L130 9.99764C128.228 6.95784 126.068 3.75491 123 2C119.932 0.245087 116.542 0 113 0C109.458 0 106.068 0.245087 103 2C99.9323 3.75491 96.7717 6.95784 95 9.99764L2 170.001C0.226979 173.04 0.00154312 176.488 1.90993e-06 179.999C-0.0015393 183.51 0.229648 186.959 2 190C3.77035 193.04 6.93245 195.244 10 197C13.0675 198.756 16.4578 200 20 200H90C117.737 200 137.925 187.558 152 164L186 105L204 74L259 168H186L168 200ZM89 168H40L113 42L150 105L125.491 147.725C116.144 163.01 105.488 168 89 168Z" fill="#00DC82" />
<path d="M958 60.0001H938C933.524 60.0001 929.926 59.9395 927 63C924.074 65.8905 925 67.5792 925 72V141C925 151.372 923.648 156.899 919 162C914.352 166.931 908.468 169 899 169C889.705 169 882.648 166.931 878 162C873.352 156.899 873 151.372 873 141V72.0001C873 67.5793 872.926 65.8906 870 63.0001C867.074 59.9396 863.476 60.0001 859 60.0001H840V141C840 159.023 845.016 173.458 855 184C865.156 194.542 879.893 200 899 200C918.107 200 932.844 194.542 943 184C953.156 173.458 958 159.023 958 141V60.0001Z" fill="#00DC82" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M1000 60.0233L1020 60V77L1020 128V156.007L1020 181L1020 189.004C1020 192.938 1019.98 194.429 1017 197.001C1014.02 199.725 1009.56 200 1005 200H986.001V181.006L986 130.012V70.0215C986 66.1576 986.016 64.5494 989 62.023C991.819 59.6358 995.437 60.0233 1000 60.0233Z" fill="#00DC82" />
</svg>
</template>

View File

@@ -1,9 +1,13 @@
<template>
<svg width="312" height="78" viewBox="0 0 312 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M65.6381 78H109.132C110.513 78.0002 111.87 77.6398 113.067 76.9552C114.263 76.2705 115.257 75.2857 115.947 74.0998C116.637 72.9139 117.001 71.5688 117 70.1996C116.999 68.8305 116.635 67.4856 115.944 66.3003L86.7343 16.1575C86.0439 14.9719 85.0508 13.9873 83.8547 13.3028C82.6586 12.6183 81.3017 12.2579 79.9205 12.2579C78.5393 12.2579 77.1825 12.6183 75.9864 13.3028C74.7903 13.9873 73.7971 14.9719 73.1067 16.1575L65.6381 28.9873L51.0356 3.89908C50.3446 2.71356 49.351 1.72914 48.1546 1.04472C46.9581 0.360308 45.6011 0 44.2196 0C42.8382 0 41.4811 0.360308 40.2847 1.04472C39.0883 1.72914 38.0947 2.71356 37.4037 3.89908L1.05644 66.3003C0.364965 67.4856 0.000601816 68.8305 7.44871e-07 70.1996C-0.000600326 71.5688 0.362582 72.9139 1.05302 74.0998C1.74346 75.2857 2.7368 76.2705 3.93315 76.9552C5.12949 77.6398 6.48665 78.0002 7.86812 78H35.1699C45.9872 78 53.9645 73.2907 59.4537 64.1032L72.7803 41.2289L79.9184 28.9873L101.341 65.7584H72.7803L65.6381 78ZM34.7248 65.7458L15.6717 65.7416L44.2324 16.7162L58.483 41.2289L48.9416 57.6127C45.2963 63.5739 41.155 65.7458 34.7248 65.7458Z" fill="currentColor" />
<path d="M175.417 77.3598V66.9562H149.03V21.3406H136.5V77.3598H175.417Z" fill="currentColor" />
<path d="M198.81 78C203.706 78 208.103 76.0793 210.178 73.1183V77.3598H221.795V37.026H210.178V41.0274C207.854 38.2264 203.706 36.3858 198.644 36.3858C186.446 36.3858 179.061 44.6286 179.061 57.1929C179.061 69.7572 186.446 78 198.81 78ZM200.635 68.3967C194.495 68.3967 190.429 63.9152 190.429 57.1929C190.429 50.3906 194.495 45.909 200.635 45.909C206.859 45.909 210.925 50.3906 210.925 57.1929C210.925 63.9152 206.859 68.3967 200.635 68.3967Z" fill="currentColor" />
<path d="M254.606 78C266.97 78 274.604 69.7572 274.604 57.1929C274.604 44.6286 266.97 36.3858 254.772 36.3858C249.544 36.3858 245.478 38.3064 243.155 41.2674V19.5H231.621V77.3598H243.155V72.9583C245.478 76.0793 249.793 78 254.606 78ZM252.78 68.3967C246.557 68.3967 242.491 63.9152 242.491 57.1929C242.491 50.3906 246.557 45.909 252.78 45.909C258.838 45.909 262.987 50.3906 262.987 57.1929C262.987 63.9152 258.838 68.3967 252.78 68.3967Z" fill="currentColor" />
<path d="M295.736 78C305.528 78 312 72.9583 312 65.2757C312 47.0294 289.098 56.3926 289.098 48.1498C289.098 45.5889 291.339 44.2285 294.575 44.2285C297.728 44.2285 300.964 46.0691 301.462 49.5103H311.502C311.087 41.5876 304.283 36.3858 294.243 36.3858C285.696 36.3858 279.307 41.4275 279.307 48.5499C279.307 65.5157 301.794 57.433 301.794 65.6758C301.794 67.9166 299.304 69.5971 295.736 69.5971C291.422 69.5971 288.517 67.3564 288.102 63.7551H278.145C278.56 72.4781 285.53 78 295.736 78Z" fill="currentColor" />
<svg width="1240" height="200" viewBox="0 0 1240 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M168 200H279C282.542 200 285.932 198.756 289 197C292.068 195.244 295.23 193.041 297 190C298.77 186.959 300.002 183.51 300 179.999C299.998 176.488 298.773 173.04 297 170.001L222 41C220.23 37.96 218.067 35.7552 215 34C211.933 32.2448 207.542 31 204 31C200.458 31 197.067 32.2448 194 34C190.933 35.7552 188.77 37.96 187 41L168 74L130 9.99764C128.228 6.95784 126.068 3.75491 123 2C119.932 0.245087 116.542 0 113 0C109.458 0 106.068 0.245087 103 2C99.9323 3.75491 96.7717 6.95784 95 9.99764L2 170.001C0.226979 173.04 0.00154312 176.488 1.90993e-06 179.999C-0.0015393 183.51 0.229648 186.959 2 190C3.77035 193.04 6.93245 195.244 10 197C13.0675 198.756 16.4578 200 20 200H90C117.737 200 137.925 187.558 152 164L186 105L204 74L259 168H186L168 200ZM89 168H40L113 42L150 105L125.491 147.725C116.144 163.01 105.488 168 89 168Z" fill="currentColor" />
<path d="M375 200C377.16 200 379 198.209 379 196V103C379 103 384 112 393 127L432 194C433.785 197.74 437.744 200 441 200H468V50H441C439.202 50 437 51.4941 437 54V148L419 116L383 55C381.248 51.8912 377.479 50 374 50H348V200H375Z" fill="currentColor" />
<path d="M724 92H737C740.314 92 743 89.3137 743 86V60H771V92H798V116H771V159C771 169.5 776.057 174 785 174H798V200H781C757.948 200 743 185.071 743 160V116H724V92Z" fill="currentColor" />
<path d="M589 154V92H573L572.832 92.0002L572.498 92.001H572.497C571.979 92.0023 571.294 92.004 571 92L570.912 91.9988C567.987 91.9592 565.941 91.9315 564 94C561.769 96.378 561 98.5652 561 102V154C561 162.059 560.543 167.037 557 171C553.457 174.831 550.217 176 543 176C535.914 176 531.543 174.831 528 171C524.457 167.037 524 162.059 524 154V102C524 98.5652 523.231 97.2459 521 95C518.769 92.622 515.412 92 512 92H496V154C496 168.004 501.389 179.809 509 188C516.742 196.191 528.434 200 543 200C557.566 200 568.258 196.191 576 188C583.742 179.809 589 168.004 589 154Z" fill="currentColor" />
<path d="M674 144L708 92H682C678.723 92 675.812 93.1758 674 96L658 120L643 97C641.188 94.1758 637.277 92 634 92H609L643 143L606 200H632C635.25 200 638.182 196.787 640 194L658 167L677 195C678.818 197.787 681.75 200 685 200H711L674 144Z" fill="currentColor" />
<path d="M931 200V175H868V66C868 62.6863 865.314 60 862 60H838V194C838 197.314 840.686 200 844 200H931Z" fill="currentColor" />
<path d="M1202 200C1225.14 200 1240 187.277 1240 169C1240 143.04 1220.69 140.838 1205.16 139.067C1194.72 137.877 1186 136.882 1186 129C1186 122.908 1190.35 120 1198 120C1205.45 120 1213.82 123.813 1215 132H1232.68C1236.12 132 1238.91 129.086 1238.06 125.757C1234.16 110.512 1218.99 101 1198 101C1177.8 101 1163 113.056 1163 130C1163 153.784 1181.4 156.618 1196.52 158.946C1207.06 160.569 1216 161.946 1216 170C1216 175.331 1209.43 179 1201 179C1190.8 179 1183.98 174.567 1183 166H1166.29C1162.87 166 1160.08 168.888 1160.81 172.233C1164.58 189.368 1180.39 200 1202 200Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M1151 149C1151 179.068 1133.34 200 1104 200C1092.58 200 1081.51 195.469 1076 188V200H1049V60H1076V111C1081.51 103.914 1091.59 100 1104 100C1132.95 100 1151 118.932 1151 149ZM1075 150C1075 166.088 1084.23 177 1099 177C1113.37 177 1123 166.088 1123 150C1123 133.721 1113.37 122 1099 122C1084.23 122 1075 133.721 1075 150Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M1005 105C998.647 101.954 991.004 101 983 101C974.487 101 967.353 101.954 961 105C954.647 108.046 949.558 112.669 946 118C943.659 121.424 941.966 124.9 940.973 128.721C940.105 132.063 942.911 135 946.364 135H963C963.254 130.685 964.951 127.919 968 125C971.176 122.081 975.537 120 981 120C986.336 120 990.951 121.462 994 124C997.049 126.412 999 129.938 999 134C999 136.031 998.271 137.604 997 139C995.729 140.269 993.287 141 991 141H975C964.454 141 956.48 143.542 950 149C943.647 154.331 940 161.608 940 171C940 176.458 941.332 181.558 944 186C946.668 190.315 950.299 193.462 955 196C959.828 198.412 965.901 200 972 200C978.607 200 984.172 198.667 989 196.002C993.955 193.21 997.348 189.442 999 185V200H1025V137C1025 129.892 1022.68 123.331 1019 118C1015.44 112.542 1011.35 107.919 1005 105ZM993.173 174.679C989.615 178.74 984.66 180.771 978.307 180.771C974.623 180.771 971.573 179.819 969.159 177.915C966.745 175.885 965.538 173.283 965.538 170.11C965.538 166.429 966.809 163.446 969.35 161.162C971.891 158.877 975.194 157.735 979.26 157.735H998.7V159.067C998.7 165.413 996.857 170.617 993.173 174.679Z" fill="currentColor" />
</svg>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<svg width="264" height="264" viewBox="0 0 264 264" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M146.496 211.2H234.822C237.627 211.2 240.383 210.468 242.813 209.078C245.242 207.688 247.259 205.688 248.662 203.279C250.064 200.871 250.801 198.139 250.8 195.359C250.799 192.579 250.059 189.847 248.655 187.44L189.337 85.612C187.935 83.2043 185.918 81.2049 183.489 79.8147C181.06 78.4246 178.305 77.6927 175.5 77.6927C172.695 77.6927 169.94 78.4246 167.511 79.8147C165.082 81.2049 163.065 83.2043 161.663 85.612L146.496 111.666L116.841 60.7179C115.438 58.3104 113.42 56.3113 110.991 54.9214C108.561 53.5315 105.805 52.7998 103 52.7998C100.195 52.7998 97.4386 53.5315 95.0089 54.9214C92.5793 56.3113 90.5615 58.3104 89.1583 60.7179L15.3453 187.44C13.9411 189.847 13.2012 192.579 13.2 195.359C13.1987 198.139 13.9363 200.871 15.3384 203.279C16.7405 205.688 18.7578 207.688 21.1873 209.078C23.6168 210.468 26.3728 211.2 29.1783 211.2H84.6219C106.589 211.2 122.789 201.636 133.937 182.979L161 136.526L175.496 111.666L219 186.34H161L146.496 211.2ZM83.7181 186.314L45.0255 186.306L103.026 86.7466L131.966 136.526L112.589 169.798C105.186 181.904 96.7763 186.314 83.7181 186.314Z" fill="currentColor" />
</svg>
</template>

View File

@@ -0,0 +1,33 @@
<script lang="ts" setup>
defineOptions({
inheritAttrs: false
})
defineProps({
title: {
type: String,
required: true
},
description: {
type: String,
required: true
}
})
</script>
<template>
<div class="w-full h-full flex flex-col justify-between items-start bg-[#020420] p-20 pt-32 pb-16">
<div
style="position: absolute;width: 1156px;height: 1000px;left: -215px;top: -337px;background: radial-gradient(50% 50% at 50% 50%, #00DC82 0%, rgba(0, 220, 130, 0) 100%);filter: blur(180.5px);opacity: 0.5;"
/>
<div>
<h1 class="text-8xl mb-4 text-white">
{{ title }}
</h1>
<p class="text-5xl text-gray-200 leading-tight pr-10">
{{ description }}
</p>
</div>
<LogoGreen class="w-[306px] h-[60px] text-white" />
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<UPopover>
<UPopover mode="hover">
<template #default="{ open }">
<UButton color="gray" variant="ghost" square :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<UIcon name="i-heroicons-swatch-20-solid" class="w-5 h-5 text-primary-500 dark:text-primary-400" />

View File

@@ -1,11 +1,11 @@
<template>
<UTooltip :text="color.value" class="capitalize" :open-delay="500">
<UButton
color="gray"
color="transparent"
square
:ui="{
color: {
gray: {
transparent: {
solid: 'bg-gray-100 dark:bg-gray-800',
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
}

View File

@@ -8,16 +8,17 @@
v-model="componentProps[prop.name]"
:name="`prop-${prop.name}`"
tabindex="-1"
:ui="{ wrapper: 'relative flex items-start justify-center' }"
class="justify-center"
/>
<USelectMenu
v-else-if="prop.type === 'string' && prop.options.length"
v-else-if="prop.options.length && prop.name !== 'label'"
v-model="componentProps[prop.name]"
:options="prop.options"
:name="`prop-${prop.name}`"
variant="none"
:ui-menu="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md', wrapper: 'relative inline-flex' }"
class="!py-0"
class="inline-flex"
:ui-menu="{ width: 'w-32 !-mt-px', rounded: 'rounded-t-none' }"
select-class="py-0"
tabindex="-1"
:popper="{ strategy: 'fixed', placement: 'bottom-start' }"
/>
@@ -28,7 +29,7 @@
:name="`prop-${prop.name}`"
variant="none"
autocomplete="off"
class="!py-0"
input-class="py-0"
tabindex="-1"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
/>
@@ -40,9 +41,7 @@
<ContentSlot v-if="$slots.default" :use="$slots.default" />
<template v-for="slot in Object.keys(slots || {})" :key="slot" #[slot]>
<ClientOnly>
<ContentSlot v-if="$slots[slot]" :use="$slots[slot]" />
</ClientOnly>
<ContentSlot :name="slot" />
</template>
</component>
</div>
@@ -114,7 +113,7 @@ const componentProps = reactive({ ...props.props })
const appConfig = useAppConfig()
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1]
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
@@ -164,9 +163,7 @@ const code = computed(() => {
continue
}
const prop = meta?.meta?.props?.find((prop: any) => prop.name === key)
code += ` ${(prop?.type === 'boolean' && value !== true) || typeof value === 'object' ? ':' : ''}${key === 'modelValue' ? 'value' : useKebabCase(key)}${prop?.type === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
code += ` ${(typeof value === 'boolean' && value !== true) || typeof value === 'object' || typeof value === 'number' ? ':' : ''}${key === 'modelValue' ? 'value' : useKebabCase(key)}${typeof value === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
}
if (props.slots) {

View File

@@ -16,7 +16,7 @@ const props = defineProps({
const appConfig = useAppConfig()
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1]
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`

View File

@@ -1,20 +1,7 @@
<template>
<div>
<FieldGroup>
<Field v-for="prop in metaProps" :key="prop.name" v-bind="prop">
<code v-if="prop.default">{{ prop.default }}</code>
<Collapsible v-if="prop.schema?.kind === 'array' && prop.schema?.schema?.filter(schema => schema.kind === 'object').length">
<FieldGroup v-for="schema in prop.schema.schema" :key="schema.name" class="!mt-0">
<Field v-for="subProp in Object.values(schema.schema)" :key="(subProp as any).name" v-bind="subProp" />
</FieldGroup>
</Collapsible>
<Collapsible v-else-if="prop.schema?.kind === 'object' && Object.values(prop.schema.schema)?.length">
<FieldGroup class="!mt-0">
<Field v-for="subProp in Object.values(prop.schema.schema)" :key="(subProp as any).name" v-bind="subProp" />
</FieldGroup>
</Collapsible>
</Field>
<ComponentPropsField v-for="prop in meta?.meta?.props" :key="prop.name" :prop="prop" />
</FieldGroup>
</div>
</template>
@@ -29,13 +16,9 @@ const props = defineProps({
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1]
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
const meta = await fetchComponentMeta(name)
const metaProps = computed(() => useSortBy(meta?.meta?.props || [], [
prop => ['string', 'number', 'boolean', 'any'].indexOf(prop.type)
]))
</script>

View File

@@ -0,0 +1,43 @@
<template>
<Field v-bind="prop">
<code v-if="prop.default">{{ prop.default }}</code>
<Collapsible v-if="prop.schema?.kind === 'array' && prop.schema?.schema?.filter(schema => schema.kind === 'object').length">
<FieldGroup v-for="schema in prop.schema.schema" :key="schema.name" class="!mt-0">
<ComponentPropsField v-for="subProp in Object.values(schema.schema)" :key="(subProp as any).name" :prop="subProp" />
</FieldGroup>
</Collapsible>
<Collapsible v-else-if="prop.schema?.kind === 'array' && prop.schema?.schema?.filter(schema => schema.kind === 'array').length">
<FieldGroup v-for="schema in prop.schema.schema" :key="schema.name" class="!mt-0">
<template v-for="subSchema in schema.schema" :key="subSchema.name">
<ComponentPropsField v-for="subProp in Object.values(subSchema.schema)" :key="(subProp as any).name" :prop="subProp" />
</template>
</FieldGroup>
</Collapsible>
<Collapsible v-else-if="prop.schema?.kind === 'object' && prop.schema.type !== 'Function' && Object.values(prop.schema.schema)?.length">
<FieldGroup class="!mt-0">
<ComponentPropsField v-for="subProp in Object.values(prop.schema.schema)" :key="(subProp as any).name" :prop="subProp" />
</FieldGroup>
</Collapsible>
<div v-else-if="prop.schema?.kind === 'enum' && prop.schema.type !== 'boolean' && startsWithCapital(prop.schema.type)" class="space-x-1 leading-7 -my-1">
<code v-for="value in prop.schema.schema" :key="value">{{ value }}</code>
</div>
</Field>
</template>
<script setup lang="ts">
defineProps({
prop: {
type: Object as PropType<any>,
default: () => ({})
}
})
function startsWithCapital (word) {
if (word.charAt(0).startsWith('"')) {
return false
}
return word.charAt(0) === word.charAt(0).toUpperCase()
}
</script>

View File

@@ -27,7 +27,7 @@ const props = defineProps({
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1]
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`

View File

@@ -38,14 +38,10 @@ const items = [{
</template>
<template #getting-started>
<div class="flex flex-col justify-center items-center gap-1">
<NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
<div class="text-gray-900 dark:text-white text-center">
<Logo class="w-auto h-8 mx-auto" />
<span class="hidden sm:block">NuxtLabs</span><span class="sm:text-primary-500 dark:sm:text-primary-400">UI</span>
</NuxtLink>
<p class="text-sm text-gray-500 dark:text-gray-400">
<p class="text-sm text-gray-500 dark:text-gray-400 mt-2">
Fully styled and customizable components for Nuxt.
</p>
</div>
@@ -57,7 +53,7 @@ const items = [{
Installation
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
Install <code>@nuxthq/ui</code> dependency to your project:
Install <code>@nuxt/ui</code> dependency to your project:
</p>
<p>
{{ description }}
@@ -65,9 +61,9 @@ const items = [{
</div>
<div class="flex flex-col items-center">
<code>$ npm install @nuxtlabs/ui</code>
<code>$ nnpm install -D @nuxthq/ui</code>
<code>$ pnpm i -D @nuxthq/ui</code>
<code>$ npm install @nuxt/ui</code>
<code>$ nnpm install -D @nuxt/ui</code>
<code>$ pnpm i -D @nuxt/ui</code>
</div>
</template>
</UAccordion>

View File

@@ -1,34 +1,34 @@
<script setup>
const router = useRouter()
const toast = useToast()
const commandPaletteRef = ref()
const users = [
{ id: 'benjamincanac', label: 'benjamincanac', href: 'https://github.com/benjamincanac', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' } },
{ id: 'Atinux', label: 'Atinux', href: 'https://github.com/Atinux', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' } },
{ id: 'smarroufin', label: 'smarroufin', href: 'https://github.com/smarroufin', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' } }
{ id: 'benjamincanac', label: 'benjamincanac', href: 'https://github.com/benjamincanac', target: '_blank', avatar: { src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/benjamincanac', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac 2x' } },
{ id: 'Atinux', label: 'Atinux', href: 'https://github.com/Atinux', target: '_blank', avatar: { src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/Atinux', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux 2x' } },
{ id: 'smarroufin', label: 'smarroufin', href: 'https://github.com/smarroufin', target: '_blank', avatar: { src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/smarroufin', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin 2x' } }
]
const actions = [
{ id: 'new-file', label: 'Add new file', icon: 'i-heroicons-document-plus', click: () => alert('New file'), shortcuts: ['⌘', 'N'] },
{ id: 'new-folder', label: 'Add new folder', icon: 'i-heroicons-folder-plus', click: () => alert('New folder'), shortcuts: ['⌘', 'F'] },
{ id: 'hashtag', label: 'Add hashtag', icon: 'i-heroicons-hashtag', click: () => alert('Add hashtag'), shortcuts: ['⌘', 'H'] },
{ id: 'label', label: 'Add label', icon: 'i-heroicons-tag', click: () => alert('Add label'), shortcuts: ['⌘', 'L'] }
{ id: 'new-file', label: 'Add new file', icon: 'i-heroicons-document-plus', click: () => toast.add({ title: 'New file added!' }), shortcuts: ['⌘', 'N'] },
{ id: 'new-folder', label: 'Add new folder', icon: 'i-heroicons-folder-plus', click: () => toast.add({ title: 'New folder added!' }), shortcuts: ['⌘', 'F'] },
{ id: 'hashtag', label: 'Add hashtag', icon: 'i-heroicons-hashtag', click: () => toast.add({ title: 'Hashtag added!' }), shortcuts: ['⌘', 'H'] },
{ id: 'label', label: 'Add label', icon: 'i-heroicons-tag', click: () => toast.add({ title: 'Label added!' }), shortcuts: ['⌘', 'L'] }
]
const groups = computed(() => commandPaletteRef.value?.query
? [{
const groups = computed(() =>
[commandPaletteRef.value?.query ? {
key: 'users',
commands: users
}]
: [{
} : {
key: 'recent',
label: 'Recent searches',
commands: users.slice(0, 1)
}, {
key: 'actions',
commands: actions
}])
}].filter(Boolean))
function onSelect (option) {
if (option.click) {

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { FormError } from '@nuxthq/ui/dist/runtime/types'
import type { FormError, FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const state = ref({
email: undefined,
@@ -14,20 +14,17 @@ const validate = (state: any): FormError[] => {
return errors
}
const form = ref()
async function submit () {
await form.value!.validate()
// Do something with state.value
async function submit (event: FormSubmitEvent<any>) {
// Do something with data
console.log(event.data)
}
</script>
<template>
<UForm
ref="form"
:validate="validate"
:state="state"
@submit.prevent="submit"
@submit="submit"
>
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { z } from 'zod'
import type { Form } from '@nuxthq/ui/dist/runtime/types'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const options = [
{ label: 'Option 1', value: 'option-1' },
@@ -44,11 +44,11 @@ const schema = z.object({
type Schema = z.infer<typeof schema>
const form = ref<Form<Schema>>()
const form = ref()
async function submit () {
await form.value!.validate()
// Do something with state.value
async function submit (event: FormSubmitEvent<Schema>) {
// Do something with event.data
console.log(event.data)
}
</script>
@@ -57,7 +57,7 @@ async function submit () {
ref="form"
:schema="schema"
:state="state"
@submit.prevent="submit"
@submit="submit"
>
<UFormGroup name="input" label="Input">
<UInput v-model="state.input" />
@@ -96,5 +96,9 @@ async function submit () {
<UButton type="submit">
Submit
</UButton>
<UButton variant="outline" class="ml-2" @click="form.clear()">
Clear
</UButton>
</UForm>
</template>

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import { ref } from 'vue'
import Joi from 'joi'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const schema = Joi.object({
emailJoi: Joi.string().required(),
passwordJoi: Joi.string()
email: Joi.string().required(),
password: Joi.string()
.min(8)
.required()
})
@@ -14,26 +15,23 @@ const state = ref({
password: undefined
})
const form = ref()
async function submit () {
await form.value!.validate()
// Do something with state.value
async function submit (event: FormSubmitEvent<any>) {
// Do something with event.data
console.log(event.data)
}
</script>
<template>
<UForm
ref="form"
:schema="schema"
:state="state"
@submit.prevent="submit"
@submit="submit"
>
<UFormGroup label="Email" name="emailJoi">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
<UFormGroup label="Password" name="passwordJoi">
<UFormGroup label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormGroup>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { ref } from 'vue'
import { string, object, email, minLength, Input } from 'valibot'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const schema = object({
email: string([email('Invalid email')]),
password: string([minLength(8, 'Must be at least 8 characters')])
})
type Schema = Input<typeof schema>
const state = ref({
email: undefined,
password: undefined
})
async function submit (event: FormSubmitEvent<Schema>) {
// Do something with event.data
console.log(event.data)
}
</script>
<template>
<UForm
:schema="schema"
:state="state"
@submit="submit"
>
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
<UFormGroup label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormGroup>
<UButton type="submit">
Submit
</UButton>
</UForm>
</template>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { ref } from 'vue'
import { object, string, InferType } from 'yup'
import type { Form } from '@nuxthq/ui/dist/runtime/types'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const schema = object({
emailYup: string().email('Invalid email').required('Required'),
passwordYup: string()
email: string().email('Invalid email').required('Required'),
password: string()
.min(8, 'Must be at least 8 characters')
.required('Required')
})
@@ -17,26 +17,23 @@ const state = ref({
password: undefined
})
const form = ref<Form<Schema>>()
async function submit () {
await form.value!.validate()
// Do something with state.value
async function submit (event: FormSubmitEvent<Schema>) {
// Do something with event.data
console.log(event.data)
}
</script>
<template>
<UForm
ref="form"
:schema="schema"
:state="state"
@submit.prevent="submit"
@submit="submit"
>
<UFormGroup label="Email" name="emailYup">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
<UFormGroup label="Password" name="passwordYup">
<UFormGroup label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormGroup>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { ref } from 'vue'
import { z } from 'zod'
import type { Form } from '@nuxthq/ui/dist/runtime/types'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const schema = z.object({
emailZod: z.string().email('Invalid email'),
passwordZod: z.string().min(8, 'Must be at least 8 characters')
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Must be at least 8 characters')
})
type Schema = z.output<typeof schema>
@@ -15,26 +15,23 @@ const state = ref({
password: undefined
})
const form = ref<Form<Schema>>()
async function submit () {
await form.value!.validate()
// Do something with state.value
async function submit (event: FormSubmitEvent<Schema>) {
// Do something with data
console.log(event.data)
}
</script>
<template>
<UForm
ref="form"
:schema="schema"
:state="state"
@submit.prevent="submit"
@submit="submit"
>
<UFormGroup label="Email" name="emailZod">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
<UFormGroup label="Password" name="passwordZod">
<UFormGroup label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormGroup>

View File

@@ -0,0 +1,9 @@
<template>
<UFormGroup v-slot="{ error }" label="Email" :error="!email && 'You must enter an email'" help="This is a nice email!">
<UInput v-model="email" type="email" placeholder="Enter email" :trailing-icon="error ? 'i-heroicons-exclamation-triangle-20-solid' : undefined" />
</UFormGroup>
</template>
<script setup lang="ts">
const email = ref('')
</script>

View File

@@ -1,5 +1,12 @@
<template>
<UInput v-model="q" name="q" placeholder="Search..." icon="i-heroicons-magnifying-glass-20-solid" :ui="{ icon: { trailing: { pointer: '' } } }">
<UInput
v-model="q"
name="q"
placeholder="Search..."
icon="i-heroicons-magnifying-glass-20-solid"
autocomplete="off"
:ui="{ icon: { trailing: { pointer: '' } } }"
>
<template #trailing>
<UButton
v-show="q !== ''"

View File

@@ -0,0 +1,33 @@
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<UModal v-model="isOpen" fullscreen>
<UCard
:ui="{
base: 'h-full flex flex-col',
rounded: '',
divide: 'divide-y divide-gray-100 dark:divide-gray-800',
body: {
base: 'grow'
}
}"
>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Modal
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
</div>
</template>
<Placeholder class="h-full" />
</UCard>
</UModal>
</div>
</template>

View File

@@ -0,0 +1,100 @@
<script setup>
const options = ref([
{ id: 1, name: 'bug', color: 'd73a4a' },
{ id: 2, name: 'documentation', color: '0075ca' },
{ id: 3, name: 'duplicate', color: 'cfd3d7' },
{ id: 4, name: 'enhancement', color: 'a2eeef' },
{ id: 5, name: 'good first issue', color: '7057ff' },
{ id: 6, name: 'help wanted', color: '008672' },
{ id: 7, name: 'invalid', color: 'e4e669' },
{ id: 8, name: 'question', color: 'd876e3' },
{ id: 9, name: 'wontfix', color: 'ffffff' }
])
const selected = ref([])
const labels = computed({
get: () => selected.value,
set: async (labels) => {
const promises = labels.map(async (label) => {
if (label.id) {
return label
}
// In a real app, you would make an API call to create the label
const response = {
name: label.name,
color: generateColorFromString(label.name)
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
function hashCode (str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return hash
}
function intToRGB (i) {
const c = (i & 0x00FFFFFF)
.toString(16)
.toUpperCase()
return '00000'.substring(0, 6 - c.length) + c
}
function generateColorFromString (str) {
return intToRGB(hashCode(str))
}
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
multiple
searchable
creatable
>
<template #label>
<template v-if="labels.length">
<span class="flex items-center -space-x-1">
<span v-for="label of labels" :key="label.id" class="flex-shrink-0 w-2 h-2 mt-px rounded-full" :style="{ background: `#${label.color}` }" />
</span>
<span>{{ labels.length }} label{{ labels.length > 1 ? 's' : '' }}</span>
</template>
<template v-else>
<span class="text-gray-500 dark:text-gray-400 truncate">Select labels</span>
</template>
</template>
<template #option="{ option }">
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full"
:style="{ background: `#${option.color}` }"
/>
<span class="truncate">{{ option.name }}</span>
</template>
<template #option-create="{ option }">
<span class="flex-shrink-0">New label:</span>
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full -mx-1"
:style="{ background: `#${generateColorFromString(option.name)}` }"
/>
<span class="block truncate">{{ option.name }}</span>
</template>
</USelectMenu>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people" searchable>
<template #option-empty="{ query }">
<q>{{ query }}</q> not found
</template>
</USelectMenu>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
const people = [
{ name: 'Wade Cooper', online: true },
{ name: 'Arlene Mccoy', online: false },
{ name: 'Devon Webb', online: false },
{ name: 'Tom Cook', online: true },
{ name: 'Tanya Fox', online: false },
{ name: 'Hellen Schmidt', online: true },
{ name: 'Caroline Schultz', online: true },
{ name: 'Mason Heaney', online: false },
{ name: 'Claudie Smitham', online: true },
{ name: 'Emil Schaefer', online: false }
]
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-model="selected" :options="people" option-attribute="name">
<template #label>
<span :class="[selected.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
<span class="truncate">{{ selected.name }}</span>
</template>
<template #option="{ option: person }">
<span :class="[person.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
<span class="truncate">{{ person.name }}</span>
</template>
</USelectMenu>
</template>

View File

@@ -78,7 +78,7 @@ const resetFilters = () => {
// Pagination
const page = ref(1)
const pageCount = ref(10)
const pageCount = ref(10)
const pageTotal = ref(200) // This value should be dynamic coming from the API
const pageFrom = computed(() => (page.value - 1) * pageCount.value + 1)
const pageTo = computed(() => Math.min(page.value * pageCount.value, pageTotal.value))
@@ -231,7 +231,7 @@ const { data: todos, pending } = await useLazyAsyncData('todos', () => $fetch<{
:total="pageTotal"
:ui="{
wrapper: 'flex items-center gap-1',
rounded: 'rounded-full min-w-[32px] justify-center',
rounded: '!rounded-full min-w-[32px] justify-center',
default: {
activeButton: {
variant: 'outline'

View File

@@ -29,12 +29,6 @@ const people = [{
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
function select (row) {

View File

@@ -4,14 +4,19 @@ const items = [{
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
disabled: true,
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
content: 'Finally, this is the content for Tab3'
}]
function onChange (index) {
const item = items[index]
alert(`${item.label} was clicked!`)
}
</script>
<template>
<UTabs :items="items" />
<UTabs :items="items" @change="onChange" />
</template>

View File

@@ -7,7 +7,7 @@ const items = [{
label: 'Password'
}]
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
const passwordForm = reactive({ currentPassword: '', newPassword: '' })
function onSubmitAccount () {

View File

@@ -9,7 +9,7 @@ const items = [{
description: 'Change your password here. After saving, you\'ll be logged out.'
}]
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
const passwordForm = reactive({ currentPassword: '', newPassword: '' })
function onSubmit (form) {

View File

@@ -0,0 +1,34 @@
<script setup>
const items = [{
label: 'Tab1',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
content: 'Finally, this is the content for Tab3'
}]
const route = useRoute()
const router = useRouter()
const selected = computed({
get () {
const index = items.findIndex((item) => item.label === route.query.tab)
if (index === -1) {
return 0
}
return index
},
set (value) {
// Hash is specified here to prevent the page from scrolling to the top
router.replace({ query: { tab: items[value].label }, hash: '#control-the-selected-index' })
}
})
</script>
<template>
<UTabs v-model="selected" :items="items" />
</template>

View File

@@ -1,11 +1,28 @@
<script setup>
const links = [{
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/benjamincanac',
srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac 2x'
},
label: 'Benjamin Canac'
label: 'benjamincanac',
to: 'https://github.com/benjamincanac',
target: '_blank'
}, {
label: 'KeJun'
avatar: {
src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/Atinux',
srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux 2x'
},
label: 'Atinux',
to: 'https://github.com/Atinux',
target: '_blank'
}, {
avatar: {
src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/smarroufin',
srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin 2x'
},
label: 'smarroufin',
to: 'https://github.com/smarroufin',
target: '_blank'
}]
const { ui } = useAppConfig()

View File

@@ -1,14 +1,13 @@
<script setup>
const commandPaletteRef = ref()
const links = inject('links')
const navigation = inject('navigation')
const files = inject('files')
const { data: files } = await useLazyAsyncData('search', () => queryContent().where({ _type: 'markdown' }).find(), { default: () => [] })
const groups = computed(() => links.value.map(item => ({
key: item.to,
const groups = computed(() => navigation.value.map(item => ({
key: item._path,
label: item.label,
commands: files.value.filter(file => file._path.startsWith(item.to)).map(file => ({
commands: files.value.filter(file => file._path.startsWith(item._path)).map(file => ({
id: file._id,
icon: 'i-heroicons-document',
title: file.navigation?.title || file.title,

View File

@@ -9,7 +9,7 @@ const items = ref(Array(55))
:total="items.length"
:ui="{
wrapper: 'flex items-center gap-1',
rounded: 'rounded-full min-w-[32px] justify-center'
rounded: '!rounded-full min-w-[32px] justify-center'
}"
:prev-button="null"
:next-button="{

View File

@@ -25,8 +25,8 @@ const links = [{
:links="links"
:ui="{
wrapper: 'border-s border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-s -ms-px lg:leading-6',
padding: 'ps-4',
base: 'group block border-s -ms-px lg:leading-6 before:hidden',
padding: 'p-0 ps-4',
rounded: '',
font: '',
ring: '',

View File

@@ -0,0 +1,99 @@
<template>
<Transition appear name="fade">
<ULandingGrid class="lg:grid-cols-10 lg:gap-8">
<div class="col-span-8 flex items-center">
<RangeExample />
</div>
<div class="col-span-2 row-span-2 flex items-center">
<RadioExample />
</div>
<div class="col-span-2">
<DropdownExampleBasic :popper="{ placement: 'bottom-start', strategy: 'absolute' }" />
</div>
<div class="col-span-6 flex flex-wrap items-center justify-between gap-1">
<UAvatarGroup :max="2">
<UAvatar
src="https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/benjamincanac 2x"
alt="benjamincanac"
/>
<UAvatar
src="https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/Atinux 2x"
alt="Atinux"
/>
<UAvatar
src="https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/smarroufin 2x"
alt="smarroufin"
/>
</UAvatarGroup>
<UButton label="Button" icon="i-heroicons-pencil-square" />
<UBadge label="Badge" />
<UColorModeToggle />
<PaginationExampleBasic />
</div>
<div class="col-span-3 row-span-8 gap-6 flex flex-col justify-between">
<UNotification :id="1" title="Notification" description="This is a notification!" icon="i-heroicons-command-line" />
<TabsExampleItemCustomSlot />
<UCard class="flex-shrink-0">
<div class="flex items-center gap-4 justify-center">
<USkeleton class="h-14 w-14 flex-shrink-0" :ui="{ rounded: 'rounded-full' }" />
<div class="space-y-3 flex-1">
<USkeleton class="h-4 w-full" />
<USkeleton class="h-4 w-2/3" />
</div>
</div>
</UCard>
</div>
<div class="col-span-5 row-span-2 flex flex-col">
<UCard :ui="{ body: { base: 'flex-1 flex flex-col overflow-y-auto', padding: '' } }" class="col-span-4 row-span-6 flex-1 flex flex-col">
<CommandPaletteExampleGroups />
</UCard>
</div>
<div class="col-span-2 row-span-2 gap-6 flex flex-col">
<CheckboxExample />
<InputExampleClearable />
<UFormGroup label="Labels">
<SelectMenuExampleCreatable />
</UFormGroup>
<UCard :ui="{ body: { padding: '!p-1' } }">
<VerticalNavigationExampleAvatarSlot />
</UCard>
</div>
<div class="col-span-7 row-span-6">
<UCard :ui="{ body: { padding: '' } }">
<TableExampleClickable :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" />
</UCard>
</div>
</ULandingGrid>
</Transition>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<Transition appear name="fade">
<div
:style="{
'--cell': `${width / cols}px`,
'--rows': rows - 1
}"
>
<div
ref="el"
class="absolute inset-0 grid justify-center auto-rows-[var(--cell)] -space-y-px"
>
<div v-for="(row, rowIndex) in grid" :key="rowIndex" class="grid grid-flow-col auto-cols-[--cell] flex-1 -space-x-px">
<div v-for="(cell, cellIndex) in row" :key="cellIndex" class="transition-[background] duration-1000 border border-primary-200/50 dark:border-primary-900/25 bg-opacity-10 hover:bg-opacity-20 dark:bg-opacity-5 dark:hover:bg-opacity-10" :class="[cell && `bg-primary-500 dark:bg-primary-400 cursor-pointer`]" @click="removeCell(rowIndex, cellIndex)" />
</div>
<div class="absolute top-[calc((var(--cell)*var(--rows))+1px)] inset-x-0 h-[calc(var(--cell)*2)] bg-gradient-to-t from-white dark:from-gray-900" />
</div>
</div>
</Transition>
</template>
<script setup>
import { useElementSize } from '@vueuse/core'
const el = ref(null)
const grid = ref([])
const rows = ref(0)
const cols = ref(0)
const colors = useAppConfig()?.ui.colors
const { width, height } = useElementSize(el)
function getRandomColor () {
return colors[Math.floor(Math.random() * (colors.length - 1))]
}
function createGrid () {
grid.value = []
for (let i = 0; i <= rows.value; i++) {
grid.value.push(new Array(cols.value).fill(null))
}
}
function createNewCell () {
const color = getRandomColor()
const x = Math.floor(Math.random() * cols.value)
grid.value[0][x] = color
}
function moveCellsDown () {
for (let row = rows.value - 1; row >= 0; row--) {
for (let col = 0; col < cols.value; col++) {
if (grid.value[row][col] !== null && grid.value[row + 1][col] === null) {
grid.value[row + 1][col] = grid.value[row][col]
grid.value[row][col] = null
}
}
}
setTimeout(() => {
if (grid.value[rows.value].every(cell => cell !== null)) {
for (let col = 0; col < cols.value; col++) {
grid.value[rows.value][col] = null
}
}
}, 500)
}
function removeCell (row, col) {
grid.value[row][col] = null
}
function calcGrid () {
const base = Math.ceil(width.value / 60)
const cell = width.value / base
rows.value = Math.ceil(height.value / cell)
cols.value = width.value / cell
createGrid()
}
watch(width, calcGrid)
onMounted(() => {
setTimeout(calcGrid, 50)
setInterval(() => {
moveCellsDown()
createNewCell()
}, 1000)
})
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,57 @@
import type { NavItem, ParsedContent } from '@nuxt/content/dist/runtime/types'
export const useContentSource = () => {
const route = useRoute()
const config = useRuntimeConfig().public
const branches = [{
name: 'dev',
icon: 'i-heroicons-cube',
suffix: 'dev',
label: 'Edge'
}, {
name: 'main',
icon: 'i-heroicons-cube',
suffix: 'latest',
label: `v${config.version}`
}]
const branch = computed(() => branches.find(b => b.name === (route.path.startsWith('/dev') ? 'dev' : 'main')))
const prefix = computed(() => `/${branch.value.name}`)
function removePrefixFromNavigation (navigation: NavItem[]): NavItem[] {
return navigation.map((link) => {
const { _path, children, ...rest } = link
return {
...rest,
_path: route.path.startsWith(prefix.value) ? _path : _path.replace(new RegExp(`^${prefix.value}`, 'g'), ''),
children: children?.length ? removePrefixFromNavigation(children) : undefined
}
})
}
function removePrefixFromFiles (files: ParsedContent[]) {
return files.map((file) => {
if (!file) {
return
}
const { _path, ...rest } = file
return {
...rest,
_path: route.path.startsWith(prefix.value) ? _path : _path.replace(new RegExp(`^${prefix.value}`, 'g'), '')
}
})
}
return {
branches,
branch,
prefix,
removePrefixFromNavigation,
removePrefixFromFiles
}
}

View File

@@ -1,9 +1,6 @@
---
title: Introduction
description: 'Fully styled and customizable components for Nuxt.'
head:
title: 'NuxtLabs UI: Fully styled and customizable components for Nuxt'
description: 'It provides everything related to UI when building your Nuxt app. This includes components, icons, colors, dark mode but also keyboard shortcuts. Built with Headless UI and Tailwind CSS, published under MIT License.'
---
This module has been developed by the [NuxtLabs](https://nuxtlabs.com/) team for [Volta](https://volta.net) and [Nuxt Studio](https://nuxt.studio/), its goal is to provide everything related to UI when building a Nuxt app. This includes components, icons, colors, dark mode but also keyboard shortcuts.

View File

@@ -4,20 +4,20 @@ description: 'Learn how to install and configure the module in your Nuxt app.'
## Quick Start
1. Install `@nuxthq/ui` dependency to your project:
1. Install `@nuxt/ui` dependency to your project:
::code-group
```sh [pnpm]
pnpm i -D @nuxthq/ui
pnpm i -D @nuxt/ui
```
```bash [yarn]
yarn add -D @nuxthq/ui
yarn add -D @nuxt/ui
```
```bash [npm]
npm install -D @nuxthq/ui
npm install -D @nuxt/ui
```
::
@@ -26,7 +26,7 @@ npm install -D @nuxthq/ui
```ts [nuxt.config]
export default defineNuxtConfig({
modules: ['@nuxthq/ui']
modules: ['@nuxt/ui']
})
```
@@ -128,7 +128,7 @@ Configure options in your `nuxt.config.ts` as such:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['@nuxthq/ui'],
modules: ['@nuxt/ui'],
ui: {
global: true,
icons: ['mdi', 'simple-icons']
@@ -138,14 +138,14 @@ export default defineNuxtConfig({
## Edge
To use the latest updates pushed on the [`dev`](https://github.com/nuxtlabs/ui/tree/dev) branch, you can use `@nuxthq/ui-edge`.
To use the latest updates pushed on the [`dev`](https://github.com/nuxt/ui/tree/dev) branch, you can use `@nuxt/ui-edge`.
Update your `package.json` to the following:
```json [package.json]
{
"devDependencies": {
"@nuxthq/ui": "npm:@nuxthq/ui-edge@latest"
"@nuxt/ui": "npm:@nuxt/ui-edge@latest"
}
}
```

View File

@@ -1,5 +1,7 @@
---
description: 'Learn how to customize the look and feel of the components.'
navigation:
badge: 'New'
---
## Overview
@@ -8,6 +10,8 @@ This module relies on Nuxt [App Config](https://nuxt.com/docs/guide/directory-st
## Colors
### Configuration
Components are based on a `primary` and a `gray` color. You can change them in your `app.config.ts`.
```ts [app.config.ts]
@@ -25,6 +29,8 @@ Try to change the `primary` and `gray` colors by clicking on the :u-icon{name="i
As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `green` and the `gray` color is `cool`.
### CSS Variables
To provide dynamic colors that can be changed at runtime, this module uses CSS variables. As Tailwind CSS already has a `gray` color, the module automatically renames it to `cool` to avoid conflicts (`coolGray` was renamed to `gray` when Tailwind CSS v3.0 was released).
Likewise, you can't define a `primary` color in your `tailwind.config.ts` as it would conflict with the `primary` color defined by the module.
@@ -33,6 +39,10 @@ Likewise, you can't define a `primary` color in your `tailwind.config.ts` as it
We'd advise you to use those colors in your components and pages, e.g. `text-primary-500 dark:text-primary-400`, `bg-gray-100 dark:bg-gray-900`, etc. so your app automatically adapts when changing your `app.config.ts`.
::
The `primary` color also has a `DEFAULT` shade that changes based on the theme. It is `500` in light mode and `400` in dark mode. You can use as a shortcut in your components and pages, e.g. `text-primary`, `bg-primary`, `focus-visible:ring-primary`, etc. :u-badge{label="New" class="!rounded-full" variant="subtle"}
### Smart Safelisting
Components having a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style), [Input](/forms/input#style) (inherited in [Select](/forms/select) and [SelectMenu](/forms/select-menu)), [Radio](/forms/radio), [Checkbox](/forms/checkbox), [Toggle](/forms/toggle), [Range](/forms/range) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.
Variant classes of those components are defined with a syntax like `bg-{color}-500 dark:bg-{color}-400` so they can be used with any color. However, this means that Tailwind will not find those classes and therefore will not generate the corresponding CSS.
@@ -67,6 +77,93 @@ export default defineNuxtConfig({
This can also happen when you bind a dynamic color to a component: `<UBadge :color="color" />`, `<UAvatar :chip-color="statuses[user.status]" />`, etc. In this case, you'll need to safelist the possible color values manually as well.
## Components
### `app.config.ts`
Components are styled with Tailwind CSS but classes are all defined in the default [app.config.ts](https://github.com/nuxt/ui/blob/dev/src/runtime/app.config.ts) file. You can override those in your own `app.config.ts`.
```ts [app.config.ts]
export default defineAppConfig({
ui: {
container: {
constrained: 'max-w-5xl'
}
}
})
```
### `ui` prop
Each component has a `ui` prop that allows you to customize everything specifically.
```vue
<template>
<UContainer :ui="{ constrained: 'max-w-2xl' }">
<slot />
</UContainer>
</template>
```
::callout{icon="i-heroicons-light-bulb"}
You can find the default classes for each component under the `Preset` section.
::
Thanks to [tailwind-merge](https://github.com/dcastil/tailwind-merge), the `ui` prop is smartly merged with the config. This means you don't have to rewrite everything. :u-badge{label="New" class="!rounded-full" variant="subtle"}
For example, the default preset of the `FormGroup` component looks like this:
```json
{
...
"label": {
"base": "block font-medium text-gray-700 dark:text-gray-200",
...
}
...
}
```
To change the font of the `label`, you only need to write:
```vue
<UFormGroup name="email" label="Email" :ui="{ label: { base: 'font-semibold' } }">
...
</UFormGroup>
```
This will smartly replace the `font-medium` by `font-semibold` and prevent any class duplication and any class priority issue.
### `class` attribute
You can also use the `class` attribute to add classes to the component.
```vue
<template>
<UButton label="Button" class="rounded-full" />
</template>
```
Again, with [tailwind-merge](https://github.com/dcastil/tailwind-merge), this will smartly merge the classes with the `ui` prop and the config. :u-badge{label="New" class="!rounded-full" variant="subtle"}
### Default values
Some component props like `size`, `color`, `variant`, etc. have a default value that you can override in your `app.config.ts`.
```ts [app.config.ts]
export default defineAppConfig({
ui: {
button: {
default: {
size: 'md',
color: 'gray',
variant: 'ghost'
}
}
}
})
```
## Dark mode
All the components are styled with dark mode in mind.
@@ -91,50 +188,6 @@ export default defineNuxtConfig({
If you're stuck in dark mode even after changing this setting, you might need to remove the `nuxt-color-mode` entry from your browser's local storage.
::
## Components
Components are styled with Tailwind CSS but classes are all defined in the default [app.config.ts](https://github.com/nuxtlabs/ui/blob/dev/src/runtime/app.config.ts) file. You can override them in your `app.config.ts`.
```ts [app.config.ts]
export default defineAppConfig({
ui: {
container: {
constrained: 'max-w-5xl'
}
}
})
```
Each component has a `ui` prop that allows you to customize everything specifically.
```vue
<template>
<UContainer :ui="{ constrained: 'max-w-2xl' }">
<slot />
</UContainer>
</template>
```
::callout{icon="i-heroicons-light-bulb"}
You can find the default classes for each component under the `Preset` section.
::
Some component props like `size`, `color`, `variant`, etc. have a default value that you can override in your `app.config.ts`.
```ts [app.config.ts]
export default defineAppConfig({
ui: {
button: {
default: {
size: 'md',
color: 'gray',
variant: 'ghost'
}
}
}
})
```
## Icons
You can use any icon (100,000+) from [Iconify](https://iconify.design/).

View File

@@ -4,7 +4,7 @@ description: Discover some real-life examples of components you can build.
---
::callout{icon="i-heroicons-wrench-screwdriver"}
If you have any ideas of examples you'd like to see, please comment on [this issue](https://github.com/nuxtlabs/ui/issues/297).
If you have any ideas of examples you'd like to see, please comment on [this issue](https://github.com/nuxt/ui/issues/297).
::
## Components
@@ -147,7 +147,7 @@ padding: false
:table-example-advanced
::
::callout{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/examples/TableExampleAdvanced.vue"}
::callout{icon="i-simple-icons-github" to="https://github.com/nuxt/ui/blob/dev/docs/components/content/examples/TableExampleAdvanced.vue"}
Take a look at the component!
::
@@ -170,7 +170,7 @@ padding: false
:command-palette-theme-algolia{class="max-h-[480px] rounded-md"}
::
::callout{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeAlgolia.vue#L23"}
::callout{icon="i-simple-icons-github" to="https://github.com/nuxt/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeAlgolia.vue#L23"}
Take a look at the component!
::
@@ -185,7 +185,7 @@ padding: false
:command-palette-theme-raycast{class="max-h-[480px] rounded-md"}
::
::callout{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeRaycast.vue#L30"}
::callout{icon="i-simple-icons-github" to="https://github.com/nuxt/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeRaycast.vue#L30"}
Take a look at the component!
::
@@ -223,8 +223,8 @@ const links = [{
:links="links"
:ui="{
wrapper: 'border-s border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-s -ms-px lg:leading-6',
padding: 'ps-4',
base: 'group block border-s -ms-px lg:leading-6 before:hidden',
padding: 'p-0 ps-4',
rounded: '',
font: '',
ring: '',
@@ -254,7 +254,7 @@ const items = ref(Array(55))
:total="items.length"
:ui="{
wrapper: 'flex items-center gap-1',
rounded: 'rounded-full min-w-[32px] justify-center'
rounded: '!rounded-full min-w-[32px] justify-center'
}"
:prev-button="null"
:next-button="{
@@ -278,6 +278,6 @@ Here are some examples of how components look like in RTL mode.
:pagination-example-r-t-l
::
::callout{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/examples/PaginationExampleRTL.vue"}
::callout{icon="i-simple-icons-github" to="https://github.com/nuxt/ui/blob/dev/docs/components/content/examples/PaginationExampleRTL.vue"}
Take a look at the component!
::

View File

@@ -1,6 +1,6 @@
---
title: Roadmap
description: Discover our Volta board for @nuxthq/ui development status.
description: Discover our Volta board for @nuxt/ui development status.
toc: false
---

View File

@@ -3,12 +3,10 @@ description: Display togglable accordion panels.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Accordion.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Accordion.vue
- label: Disclosure
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/disclosure'
navigation:
badge: New
---
## Usage
@@ -55,7 +53,7 @@ You can also pass any prop from the [Button](/elements/button) component directl
---
baseProps:
items:
- label: '1. What is NuxtLabs UI?'
- label: '1. What is Nuxt UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '2. Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
@@ -94,7 +92,7 @@ You can also set them to `null` to hide the icons.
---
baseProps:
items:
- label: '1. What is NuxtLabs UI?'
- label: '1. What is Nuxt UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '2. Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
@@ -119,7 +117,7 @@ Use the `multiple` prop to to allow multiple elements to be opened at the same t
---
baseProps:
items:
- label: 'What is NuxtLabs UI?'
- label: 'What is Nuxt UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
@@ -142,7 +140,7 @@ Use the `default-open` prop to open all items by default. Works better when the
---
baseProps:
items:
- label: 'What is NuxtLabs UI?'
- label: 'What is Nuxt UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
@@ -241,14 +239,10 @@ const items = [{
</template>
<template #getting-started>
<div class="flex flex-col justify-center items-center gap-1">
<NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
<div class="text-gray-900 dark:text-white text-center">
<Logo class="w-auto h-8 mx-auto" />
<span class="hidden sm:block">NuxtLabs</span><span class="sm:text-primary-500 dark:sm:text-primary-400">UI</span>
</NuxtLink>
<p class="text-sm text-gray-500 dark:text-gray-400">
<p class="text-sm text-gray-500 dark:text-gray-400 mt-2">
Fully styled and customizable components for Nuxt.
</p>
</div>
@@ -260,7 +254,7 @@ const items = [{
Installation
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
Install <code>@nuxthq/ui</code> dependency to your project:
Install <code>@nuxt/ui</code> dependency to your project:
</p>
<p>
{{ description }}
@@ -268,9 +262,9 @@ const items = [{
</div>
<div class="flex flex-col items-center">
<code>$ npm install @nuxtlabs/ui</code>
<code>$ nnpm install -D @nuxthq/ui</code>
<code>$ pnpm i -D @nuxthq/ui</code>
<code>$ npm install @nuxt/ui</code>
<code>$ nnpm install -D @nuxt/ui</code>
<code>$ pnpm i -D @nuxt/ui</code>
</div>
</template>
</UAccordion>

View File

@@ -3,9 +3,7 @@ description: Display an alert element to draw attention.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Alert.vue
navigation:
badge: Edge
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Alert.vue
---
## Usage
@@ -34,7 +32,7 @@ props:
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}` or change it globally in `ui.alert.default.icon`.
::component-card
---

View File

@@ -3,7 +3,9 @@ description: Display an image that represents a resource or a group of resources
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Avatar.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Avatar.vue
navigation:
badge: 'New'
---
## Usage
@@ -51,9 +53,25 @@ baseProps:
### Placeholder
If there is an error loading the `src` of the avatar or `src` is null a background placeholder will be displayed, customizable in `ui.avatar.background`.
If there is an error loading the `src` of the avatar or `src` is null / false a background placeholder will be displayed, customizable in `ui.avatar.background`.
If there's an `alt` prop initials will be displayed on top of the background, customizable in `ui.avatar.placeholder`.
#### Icon :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}` or change it globally in `ui.avatar.default.icon` to display an icon on top of the background.
::component-card
---
props:
icon: 'i-heroicons-photo'
size: 'sm'
excludedProps:
- icon
---
::
#### Alt
Otherwise, a placeholder will be displayed with the initials of the `alt` prop, customizable in `ui.avatar.placeholder`.
::component-card
---

View File

@@ -3,7 +3,7 @@ description: Display a short text to represent a status or a category.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Badge.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Badge.vue
---
## Usage
@@ -45,7 +45,7 @@ Badge
Besides all the colors from the `ui.colors` object, you can also use the `white` and `black` colors with their pre-defined variants.
#### White :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
#### White
::component-card
---
@@ -62,7 +62,7 @@ excludedProps:
Badge
::
#### Gray :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
#### Gray
::component-card
---
@@ -79,7 +79,7 @@ excludedProps:
Badge
::
#### Black :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
#### Black
::component-card
---

View File

@@ -3,7 +3,9 @@ description: Create a button with icon or link capabilities.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Button.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Button.vue
navigation:
badge: 'New'
---
## Usage
@@ -276,12 +278,14 @@ excludedProps:
To stack buttons as a group, use the `ButtonGroup` component.
- To size all the buttons equally, pass the `size` prop
- To change the orientation of the buttons, set the `orientation` prop to `vertical` :u-badge{label="New" class="!rounded-full" variant="subtle"}
- To adjust the rounded or the shadow around buttons, customize with `ui.buttonGroup.rounded` or `ui.buttonGroup.shadow`
::component-card{slug="ButtonGroup"}
---
props:
size: 'sm'
orientation: 'horizontal'
ui:
size:
2xs: ''
@@ -290,6 +294,9 @@ ui:
md: ''
lg: ''
xl: ''
orientation:
horizontal: ''
vertical: ''
code: |
<UButton label="Action" color="white" />
<UButton icon="i-heroicons-chevron-down-20-solid" color="gray" />

View File

@@ -3,7 +3,7 @@ description: Display a list of actions in a dropdown menu.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Dropdown.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Dropdown.vue
- label: Menu
icon: i-simple-icons-headlessui
to: https://headlessui.com/vue/menu

View File

@@ -3,7 +3,7 @@ description: Display an icon from Iconify library.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Icon.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Icon.vue
---
## Usage

View File

@@ -4,7 +4,7 @@ description: Display a keyboard key in a text block.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Kbd.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Kbd.vue
navigation:
title: 'Kbd'
---

View File

@@ -4,9 +4,7 @@ description: Render a NuxtLink but with superpowers.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Link.vue
navigation:
badge: Edge
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Link.vue
---
## Usage

View File

@@ -3,7 +3,7 @@ description: Display an input field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Input.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Input.vue
---
## Usage

View File

@@ -3,14 +3,14 @@ description: Collect and validate form data.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Form.ts
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Form.vue
navigation:
badge: Edge
badge: 'New'
---
## Usage
Use the Form component to validate form data using schema libraries such as [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi) or your own validation logic. It works seamlessly with the FormGroup component to automatically display error messages around form elements.
Use the Form component to validate form data using schema libraries such as [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi), [Valibot](https://valibot.dev/) or your own validation logic. It works seamlessly with the FormGroup component to automatically display error messages around form elements.
The Form component requires the `validate` and `state` props for form validation.
@@ -26,7 +26,8 @@ The Form component requires the `validate` and `state` props for form validation
#code
```vue
<script setup lang="ts">
import type { FormError } from '@nuxthq/ui/dist/runtime/types'
import { ref } from 'vue'
import type { FormError, FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const state = ref({
email: undefined,
@@ -40,20 +41,17 @@ const validate = (state: any): FormError[] => {
return errors
}
const form = ref()
async function submit () {
await form.value!.validate()
// Do something with state.value
async function submit (event: FormSubmitEvent<any>) {
// Do something with data
console.log(event.data)
}
</script>
<template>
<UForm
ref="form"
:validate="validate"
:state="state"
@submit.prevent="submit"
@submit="submit"
>
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
@@ -73,7 +71,7 @@ async function submit () {
## Schema
You can provide a schema from [Yup](#yup), [Zod](#zod) or [Joi](#joi) through the `schema` prop to validate the state. It's important to note that **none of these libraries are included** by default, so make sure to **install the one you need**.
You can provide a schema from [Yup](#yup), [Zod](#zod) or [Joi](#joi), [Valibot](#valibot) through the `schema` prop to validate the state. It's important to note that **none of these libraries are included** by default, so make sure to **install the one you need**.
### Yup
@@ -84,7 +82,9 @@ You can provide a schema from [Yup](#yup), [Zod](#zod) or [Joi](#joi) through th
#code
```vue
<script setup lang="ts">
import { ref } from 'vue'
import { object, string, InferType } from 'yup'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const schema = object({
email: string().email('Invalid email').required('Required'),
@@ -93,25 +93,24 @@ const schema = object({
.required('Required')
})
type Schema = InferType<typeof schema>
const state = ref({
email: undefined,
password: undefined
})
const form = ref()
async function submit () {
await form.value!.validate()
// Do something with state.value
async function submit (event: FormSubmitEvent<Schema>) {
// Do something with event.data
console.log(event.data)
}
</script>
<template>
<UForm
ref="form"
:schema="schema"
:state="state"
@submit.prevent="submit"
@submit="submit"
>
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
@@ -138,32 +137,33 @@ async function submit () {
#code
```vue
<script setup lang="ts">
import { ref } from 'vue'
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Must be at least 8 characters')
})
type Schema = z.output<typeof schema>
const state = ref({
email: undefined,
password: undefined
})
const form = ref()
async function submit () {
await form.value!.validate()
// Do something with state.value
async function submit (event: FormSubmitEvent<Schema>) {
// Do something with data
console.log(event.data)
}
</script>
<template>
<UForm
ref="form"
:schema="schema"
:state="state"
@submit.prevent="submit"
@submit="submit"
>
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
@@ -190,8 +190,9 @@ async function submit () {
#code
```vue
<script setup lang="ts">
import { ref } from 'vue'
import Joi from 'joi'
import type { Schema } from 'joi'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const schema = Joi.object({
email: Joi.string().required(),
@@ -205,18 +206,17 @@ const state = ref({
password: undefined
})
async function submit () {
await form.value!.validate()
// Do something with state.value
async function submit (event: FormSubmitEvent<any>) {
// Do something with event.data
console.log(event.data)
}
</script>
<template>
<UForm
ref="form"
:schema="schema"
:state="state"
@submit.prevent="submit"
@submit="submit"
>
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
@@ -234,37 +234,58 @@ async function submit () {
```
::
## Type Inference
### Valibot :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
You can utilize Zod and Yup's type inference feature to automatically infer the types of your schema and form data. This allows for strong type checking and better code validation, reducing the likelihood of errors.
::component-example
#default
:form-example-valibot{class="space-y-4 w-60"}
#code
```vue
<script setup lang="ts">
import type { Form } from '@nuxthq/ui/dist/runtime/types'
import { ref } from 'vue'
import { string, object, email, minLength, Input } from 'valibot'
import type { FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
// [...]
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Must be at least 8 characters')
const schema = object({
email: string([email('Invalid email')]),
password: string([minLength(8, 'Must be at least 8 characters')])
})
const state: Partial<Schema> = ref({
type Schema = Input<typeof schema>
const state = ref({
email: undefined,
password: undefined
})
type Schema = z.output<typeof schema>
// For Yup, use:
// type Schema = InferType<typeof schema>
const form = ref<Form<Schema>>()
async function submit() {
const data: Schema = await form.value!.validate()
// Do something with data
async function submit (event: FormSubmitEvent<Schema>) {
// Do something with event.data
console.log(event.data)
}
</script>
// [...]
<template>
<UForm
:schema="schema"
:state="state"
@submit="submit"
>
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
<UFormGroup label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormGroup>
<UButton type="submit">
Submit
</UButton>
</UForm>
</template>
```
::
## Other libraries
@@ -274,30 +295,30 @@ Here is an example with [Vuelidate](https://github.com/vuelidate/vuelidate):
```vue
<script setup lang="ts">
import useVuelidate from '@vuelidate/core';
import useVuelidate from '@vuelidate/core'
const props = defineProps({
rules: { type: Object, required: true },
model: { type: Object, required: true },
});
model: { type: Object, required: true }
})
const form = ref();
const v = useVuelidate(props.rules, props.model);
const v = useVuelidate(props.rules, props.model)
async function validateWithVuelidate() {
v.value.$touch();
await v.value.$validate();
v.value.$touch()
await v.value.$validate()
return v.value.$errors.map((error) => ({
message: error.$message,
path: error.$propertyPath,
}));
}))
}
defineExpose({
validate: async () => {
await form.value.validate();
},
});
await form.value.validate()
}
})
</script>
<template>
@@ -307,9 +328,56 @@ defineExpose({
</template>
```
## Backend validation
You can manually set errors after form submission if required. To do this, simply use the `form.setErrors` function to set the errors as needed.
```vue
<script setup lang="ts">
import type { FormError, FormSubmitEvent } from '@nuxt/ui/dist/runtime/types'
const state = ref({
email: undefined,
password: undefined
})
const form = ref()
async function submit (event: FormSubmitEvent<any>) {
form.value.clear()
const response = await fetch('...')
if (!response.status === 422) {
const errors = await response.json()
form.value.setErrors(errors.map((err) => {
// Map validation errors to { path: string, message: string }
}))
} else {
// ...
}
}
</script>
<template>
<UForm ref="form" :state="state" @submit="submit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
<UFormGroup label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormGroup>
<UButton type="submit">
Submit
</UButton>
</UForm>
</template>
```
## Input events
The Form component automatically triggers validation upon input `blur` or `change` events. This ensures that any errors are displayed as soon as the user interacts with the form elements.
The Form component automatically triggers validation upon `submit`, `input`, `blur` or `change` events. This ensures that any errors are displayed as soon as the user interacts with the form elements. You can control when validation happens this using the `validate-on` prop.
::component-example
#default
@@ -319,3 +387,23 @@ The Form component automatically triggers validation upon input `blur` or `chang
## Props
:component-props
## API
::field-group
::field{name="validate (path?: string, opts: { silent?: boolean })" type="Promise<T>"}
Triggers form validation. Will raise any errors unless `opts.silent` is set to true.
::
::field{name="clear (path?: string)" type="void"}
Clears form errors associated with a specific path. If no path is provided, clears all form errors.
::
::field{name="getErrors (path?: string)" type="FormError[]"}
Retrieves form errors associated with a specific path. If no path is provided, returns all form errors.
::
::field{name="setErrors (errors: FormError[], path?: string)" type="void"}
Sets form errors for a given path. If no path is provided, overrides all errors.
::
::field{name="errors" type="Ref<FormError[]>"}
A reference to the array containing validation errors. Use this to access or manipulate the error information.
::
::

View File

@@ -3,7 +3,7 @@ description: Display a textarea field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Textarea.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Textarea.vue
---
## Usage

View File

@@ -3,7 +3,7 @@ description: Display a select field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Select.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Select.vue
---
## Usage
@@ -30,7 +30,7 @@ const country = ref(countries[0])
```
::
When using objects, you can configure which field will be used for display through the `option-attribute` prop that defaults to `label` and which field will be used for comparison through the `value-attribute` prop that defaults to `value`.
When using objects, you can configure which field will be used for display through the `option-attribute` prop that defaults to `label` and which field will be used for comparison through the `value-attribute` prop that defaults to `value`.
Adding a `disabled` key to the objects will control the disabled state of the option.

View File

@@ -1,9 +1,10 @@
---
title: SelectMenu
description: Display a select menu with advanced features.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/SelectMenu.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/SelectMenu.vue
- label: 'Listbox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/listbox'
@@ -11,13 +12,13 @@ links:
## Usage
The SelectMenu component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as [color](/forms/select#style), [variant](/forms/select#style), [size](/forms/select#size), [placeholder](/forms/select#placeholder), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc.
The `SelectMenu` component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the `Select` props to configure the display if you don't want to override the default slot such as [color](/forms/select#style), [variant](/forms/select#style), [size](/forms/select#size), [placeholder](/forms/select#placeholder), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc.
Like the Select component, you can use the `options` prop to pass an array of strings or objects.
Like the `Select` component, you can use the `options` prop to pass an array of strings or objects.
::component-example
#default
:select-menu-example-basic{class="w-full lg:w-48"}
:select-menu-example-basic{class="w-full lg:w-40"}
#code
```vue
@@ -39,7 +40,7 @@ You can use the `multiple` prop to select multiple values.
::component-example
#default
:select-menu-example-multiple{class="w-full lg:w-48"}
:select-menu-example-multiple{class="w-full lg:w-40"}
#code
```vue
@@ -61,7 +62,7 @@ You can pass an array of objects to `options` and either compare on the whole ob
::component-example
#default
:select-menu-example-objects{class="w-full lg:w-48"}
:select-menu-example-objects{class="w-full lg:w-40"}
#code
```vue
<script setup>
@@ -108,11 +109,11 @@ const selected = ref(people[0])
```
::
If you only want to select a single object property rather than the whole object as value, you can set the `value-attribute` property. This prop defaults to `null`. :u-badge{label="Edge" class="!rounded-full" variant="subtle"}
If you only want to select a single object property rather than the whole object as value, you can set the `value-attribute` property. This prop defaults to `null`.
::component-example
#default
:select-menu-example-objects-value-attribute{class="w-full lg:w-48"}
:select-menu-example-objects-value-attribute{class="w-full lg:w-40"}
#code
```vue
@@ -159,7 +160,7 @@ Use the `selected-icon` prop to set a different icon or change it globally in `u
::component-card
---
baseProps:
class: 'w-full lg:w-48'
class: 'w-full lg:w-40'
placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
@@ -184,7 +185,7 @@ This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) compon
::component-card
---
baseProps:
class: 'w-full lg:w-48'
class: 'w-full lg:w-40'
placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
@@ -193,7 +194,7 @@ props:
---
::
### Async search :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
### Async search
Pass a function to the `searchable` prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.
@@ -201,7 +202,7 @@ Use the `debounce` prop to adjust the delay of the function.
::component-example
#default
:select-menu-example-async-search{class="w-full lg:w-48"}
:select-menu-example-async-search{class="w-full lg:w-40"}
#code
```vue
@@ -227,6 +228,106 @@ const selected = ref([])
```
::
### Create option
Use the `creatable` prop to enable the creation of new options when the search doesn't return any results (only works with `searchable`).
Try to search for something that doesn't exist in the example below.
::component-example
#default
:select-menu-example-creatable{class="w-full lg:w-40"}
#code
```vue
<script setup>
const options = ref([
{ id: 1, name: 'bug', color: 'd73a4a' },
{ id: 2, name: 'documentation', color: '0075ca' },
{ id: 3, name: 'duplicate', color: 'cfd3d7' },
{ id: 4, name: 'enhancement', color: 'a2eeef' },
{ id: 5, name: 'good first issue', color: '7057ff' },
{ id: 6, name: 'help wanted', color: '008672' },
{ id: 7, name: 'invalid', color: 'e4e669' },
{ id: 8, name: 'question', color: 'd876e3' },
{ id: 9, name: 'wontfix', color: 'ffffff' }
])
const selected = ref([])
const labels = computed({
get: () => selected.value,
set: async (labels) => {
const promises = labels.map(async (label) => {
if (label.id) {
return label
}
// In a real app, you would make an API call to create the label
const response = {
name: label.name,
color: generateColorFromString(label.name)
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
// Look at the component example to see how this is used
function generateColorFromString (str) {
// ...
}
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
multiple
searchable
creatable
>
<template #label>
<template v-if="labels.length">
<span class="flex items-center -space-x-1">
<span v-for="label of labels" :key="label.id" class="flex-shrink-0 w-2 h-2 mt-px rounded-full" :style="{ background: `#${label.color}` }" />
</span>
<span>{{ labels.length }} label{{ labels.length > 1 ? 's' : '' }}</span>
</template>
<template v-else>
<span class="text-gray-500 dark:text-gray-400 truncate">Select labels</span>
</template>
</template>
<template #option="{ option }">
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full"
:style="{ background: `#${option.color}` }"
/>
<span class="truncate">{{ option.name }}</span>
</template>
<template #option-create="{ option }">
<span class="flex-shrink-0">New label:</span>
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full -mx-1"
:style="{ background: `#${generateColorFromString(option.name)}` }"
/>
<span class="block truncate">{{ option.name }}</span>
</template>
</USelectMenu>
</template>
```
::
## Slots
### `label`
@@ -235,7 +336,7 @@ You can override the `#label` slot and handle the display yourself.
::component-example
#default
:select-menu-example-multiple-slot{class="w-full lg:w-48"}
:select-menu-example-multiple-slot{class="w-full lg:w-40"}
#code
```vue
@@ -262,7 +363,7 @@ You can also override the `#default` slot entirely.
::component-example
#default
:select-menu-example-button{class="w-full lg:w-48"}
:select-menu-example-button{class="w-full lg:w-40"}
#code
```vue
@@ -284,6 +385,83 @@ const selected = ref(people[3])
```
::
### `option`
Use the `#option` slot to customize the option content. You will have access to the `option`, `active` and `selected` properties in the slot scope.
::component-example
#default
:select-menu-example-option-slot{class="w-full lg:w-40"}
#code
```vue
<script setup>
const people = [
{ name: 'Wade Cooper', online: true },
{ name: 'Arlene Mccoy', online: false },
{ name: 'Devon Webb', online: false },
{ name: 'Tom Cook', online: true },
{ name: 'Tanya Fox', online: false },
{ name: 'Hellen Schmidt', online: true },
{ name: 'Caroline Schultz', online: true },
{ name: 'Mason Heaney', online: false },
{ name: 'Claudie Smitham', online: true },
{ name: 'Emil Schaefer', online: false }
]
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-model="selected" :options="people" option-attribute="name">
<template #label>
<span :class="[selected.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
<span class="truncate">{{ selected.name }}</span>
</template>
<template #option="{ option: person }">
<span :class="[person.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
<span class="truncate">{{ person.name }}</span>
</template>
</USelectMenu>
</template>
```
::
### `option-empty`
Use the `#option-empty` slot to customize the content displayed when the `searchable` prop is `true` and there is no options. You will have access to the `query` property in the slot scope.
::component-example
#default
:select-menu-example-option-empty-slot{class="w-full lg:w-40"}
#code
```vue
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people" searchable>
<template #option-empty="{ query }">
<q>{{ query }}</q> not found
</template>
</USelectMenu>
</template>
```
::
### `option-create`
Use the `#option-create` slot to customize the content displayed when the `creatable` prop is `true` and there is no options. You will have access to the `query` property in the slot scope.
::callout{icon="i-heroicons-light-bulb"}
An example is available in the [Create option](#create-option) section.
::
## Props
:component-props

View File

@@ -3,7 +3,7 @@ description: Display a checkbox field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Checkbox.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Checkbox.vue
---
## Usage
@@ -95,6 +95,24 @@ props:
---
::
## Slots
### `label`
Use the `#label` slot to override the content of the label.
::component-card
---
slots:
label: <span class="italic">Label</span>
baseProps:
name: 'checkbox5'
---
#label
[Label]{.italic}
::
## Props
:component-props

View File

@@ -3,7 +3,7 @@ description: Display a radio field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Radio.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Radio.vue
---
## Usage
@@ -103,12 +103,31 @@ Use the `disabled` prop to disable the Radio.
---
baseProps:
name: 'radio5'
label: 'Label'
value: true
props:
disabled: true
---
::
## Slots
### `label`
Use the `#label` slot to override the content of the label.
::component-card
---
slots:
label: <span class="italic">Label</span>
baseProps:
name: 'radio6'
---
#label
[Label]{.italic}
::
## Props
:component-props

View File

@@ -3,7 +3,7 @@ description: Display a toggle field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Toggle.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Toggle.vue
- label: 'Switch'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/switch'

View File

@@ -3,7 +3,7 @@ description: Display a range field
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Range.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Range.vue
---
## Usage

View File

@@ -1,9 +1,10 @@
---
title: FormGroup
description: Display a label and additional informations around a form element.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/FormGroup.ts
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/FormGroup.vue
---
@@ -14,8 +15,8 @@ Use the FormGroup component around an [Input](/forms/input), [Textarea](/forms/t
::component-card
---
props:
name: 'email'
label: 'Email'
name: 'email'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
@@ -111,30 +112,55 @@ Use the `error` prop to display an error message below the form element.
When used together with the `help` prop, the `error` prop will take precedence.
::component-example
#default
:form-group-error-example
#code
```vue
<template>
<UFormGroup v-slot="{ error }" label="Email" :error="!email && 'You must enter an email'" help="This is a nice email!">
<UInput v-model="email" type="email" placeholder="Enter email" :trailing-icon="error && 'i-heroicons-exclamation-triangle-20-solid'" />
</UFormGroup>
</template>
<script setup lang="ts">
const email = ref('')
</script>
```
::
::callout{icon="i-heroicons-light-bulb"}
The `error` prop will automatically set the `color` prop of the form element to `red`.
::
You can also use the `error` prop as a boolean to mark the form element as invalid.
::component-card
---
baseProps:
name: 'group-error'
props:
label: 'Email'
help: 'We will never share your email with anyone else.'
error: "Not a valid email address."
error: true
excludedProps:
- ui
- error
- label
code: >-
<UInput placeholder="you@example.com" trailing-icon="i-heroicons-exclamation-triangle-20-solid" />
<UInput placeholder="you@example.com" />
---
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com" trailing-icon="i-heroicons-exclamation-triangle-20-solid"}
:u-input{model-value="benjamincanac" placeholder="you@example.com"}
::
You can also use the `error` prop as a boolean to mark the form element as invalid.
::callout{icon="i-heroicons-light-bulb"}
The `error` prop will automatically set the `color` prop of the form element to `red`.
::callout{icon="i-heroicons-light-bulb" to="/forms/form"}
Learn more about form validation in the `Form` component.
::
### Size :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
### Size
Use the `size` prop to change the size of the label and the form element.
@@ -160,6 +186,10 @@ code: >-
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
::callout{icon="i-heroicons-exclamation-triangle"}
This will only work with form elements that support the `size` prop.
::
## Props
:component-props

View File

@@ -3,7 +3,7 @@ description: 'Display data in a table.'
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/data/Table.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/data/Table.vue
---
## Usage
@@ -75,7 +75,7 @@ Use the `columns` prop to configure which columns to display. It's an array of o
- `key` - The field to display from the row data.
- `sortable` - Whether the column is sortable. Defaults to `false`.
- `direction` - The sort direction to use on first click. Defaults to `asc`.
- `class` :u-badge{label="New" class="!rounded-full" variant="subtle"} - The class to apply to the column cells.
- `class` - The class to apply to the column cells.
::component-example{class="grid"}
---
@@ -327,7 +327,7 @@ const selected = ref([people[1]])
You can use the `by` prop to compare objects by a field instead of comparing object instances. We've replicated the behavior of Headless UI [Combobox](https://headlessui.com/vue/combobox#binding-objects-as-values).
::
You can also add a `select` listener on your Table to make the rows clickable. The function will receive the row as the first argument. :u-badge{label="New" class="!rounded-full" variant="subtle"}
You can also add a `select` listener on your Table to make the rows clickable. The function will receive the row as the first argument.
You can use this to navigate to a page, open a modal or even to select the row manually.
@@ -545,7 +545,7 @@ Even though you can customize the sort button as mentioned in the [Sortable](#so
### `<column>-data`
Use the `#<column>-data` slot to customize the data cell of a column. You will have access to the `row` and `column` properties in the slot scope.
Use the `#<column>-data` slot to customize the data cell of a column. You will have access to the `row`, `column` and `getRowData` properties in the slot scope.
You can for example create an extra column for actions with a [Dropdown](/elements/dropdown) component inside or change the color of the rows based on a selection.

View File

@@ -1,9 +1,10 @@
---
title: VerticalNavigation
description: Display a vertical navigation.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/navigation/VerticalNavigation.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/navigation/VerticalNavigation.vue
---
## Usage
@@ -120,10 +121,10 @@ const links = [{
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
},
label: 'Benjamin Canac'
}, {
label: 'KeJun'
}]
label: 'Benjamin Canac',
to: 'https://github.com/benjamincanac',
target: '_blank'
}, ...]
const { ui } = useAppConfig()
</script>

View File

@@ -1,9 +1,10 @@
---
title: CommandPalette
description: Add a customizable command palette to your app.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/navigation/CommandPalette.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/navigation/CommandPalette.vue
- label: 'Combobox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/combobox'
@@ -112,6 +113,7 @@ padding: false
```vue
<script setup>
const router = useRouter()
const toast = useToast()
const commandPaletteRef = ref()
@@ -122,25 +124,24 @@ const users = [
]
const actions = [
{ id: 'new-file', label: 'Add new file', icon: 'i-heroicons-document-plus', click: () => alert('New file'), shortcuts: ['⌘', 'N'] },
{ id: 'new-folder', label: 'Add new folder', icon: 'i-heroicons-folder-plus', click: () => alert('New folder'), shortcuts: ['⌘', 'F'] },
{ id: 'hashtag', label: 'Add hashtag', icon: 'i-heroicons-hashtag', click: () => alert('Add hashtag'), shortcuts: ['⌘', 'H'] },
{ id: 'label', label: 'Add label', icon: 'i-heroicons-tag', click: () => alert('Add label'), shortcuts: ['⌘', 'L'] }
{ id: 'new-file', label: 'Add new file', icon: 'i-heroicons-document-plus', click: () => toast.add({ title: 'New file added!' }), shortcuts: ['⌘', 'N'] },
{ id: 'new-folder', label: 'Add new folder', icon: 'i-heroicons-folder-plus', click: () => toast.add({ title: 'New folder added!' }), shortcuts: ['⌘', 'F'] },
{ id: 'hashtag', label: 'Add hashtag', icon: 'i-heroicons-hashtag', click: () => toast.add({ title: 'Hashtag added!' }), shortcuts: ['⌘', 'H'] },
{ id: 'label', label: 'Add label', icon: 'i-heroicons-tag', click: () => toast.add({ title: 'Label added!' }), shortcuts: ['⌘', 'L'] }
]
const groups = computed(() => commandPaletteRef.value?.query
? [{
key: 'users',
commands: users
}]
: [{
key: 'recent',
label: 'Recent searches',
commands: users.slice(0, 1)
}, {
key: 'actions',
commands: actions
}])
const groups = computed(() =>
[commandPaletteRef.value?.query ? {
key: 'users',
commands: users
} : {
key: 'recent',
label: 'Recent searches',
commands: users.slice(0, 1)
}, {
key: 'actions',
commands: actions
}].filter(Boolean))
function onSelect (option) {
if (option.click) {
@@ -328,6 +329,26 @@ The `loading` state will automatically be enabled when a `search` function is lo
## Slots
### `<group>-icon`
Use the `#<group>-icon` slot to override the left command content which display by default the `icon`, `avatar` and `chip`.
### `<group>-command`
Use the `#<group>-command` slot to override the command content which display by default the `prefix`, `suffix` and `label` (customizable through the `command-attribute` prop).
### `<group>-active`
Use the `#<group>-active` slot to override the right command content (when hovered) which display by default the `active` field of the group if provided.
### `<group>-inactive`
Use the `#<group>-inactive` slot to override the right command content (when not hovered) which display by default the `inactive` field of the group if provided or the `shortcuts` of the command.
::callout{icon="i-heroicons-light-bulb"}
The 4 slots above will have access to the `group`, `command`, `active` and `selected` properties in the slot scope.
::
### `empty-state`
Use the `#empty-state` slot to customize the empty state.

View File

@@ -3,7 +3,7 @@ description: Add a pagination to handle pages.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/navigation/Pagination.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/navigation/Pagination.vue
---
## Usage

View File

@@ -3,9 +3,9 @@ description: A set of tab panels that are displayed one at a time.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/navigation/Tabs.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/navigation/Tabs.vue
navigation:
badge: Edge
badge: 'New'
---
## Usage
@@ -83,6 +83,78 @@ const items = [...]
```
::
::callout{icon="i-heroicons-exclamation-triangle"}
This will have no effect if you are using a `v-model` to control the selected index.
::
### Listen to changes :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
You can listen to changes by using the `@change` event. The event will emit the index of the selected item.
::component-example
#default
:tabs-example-change{class="w-full"}
#code
```vue
<script setup>
const items = [...]
function onChange (index) {
const item = items[index]
alert(`${item.label} was clicked!`)
}
</script>
<template>
<UTabs :items="items" @change="onChange" />
</template>
```
::
### Control the selected index :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
Use a `v-model` to control the selected index.
::component-example
#default
:tabs-example-v-model{class="w-full"}
#code
```vue
<script setup>
const items = [...]
const route = useRoute()
const router = useRouter()
const selected = computed({
get () {
const index = items.findIndex((item) => item.label === route.query.tab)
if (index === -1) {
return 0
}
return index
},
set (value) {
// Hash is specified here to prevent the page from scrolling to the top
router.replace({ query: { tab: items[value].label }, hash: '#control-the-selected-index' })
}
})
</script>
<template>
<UTabs v-model="selected" :items="items" />
</template>
```
::
::callout{icon="i-heroicons-information-circle"}
In this example, we are binding tabs to the route query. Refresh the page to see the selected tab change.
::
## Slots
You can use slots to customize the buttons and items content of the Accordion.

View File

@@ -3,10 +3,12 @@ description: Display a modal within your application.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/Modal.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Modal.vue
- label: 'Dialog'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/dialog'
navigation:
badge: 'New'
---
## Usage
@@ -125,7 +127,7 @@ const isOpen = ref(false)
```
::
### Prevent close :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
### Prevent close
Use the `prevent-close` prop to disable the outside click alongside the `esc` keyboard shortcut.
@@ -178,6 +180,52 @@ defineShortcuts({
</script>
```
### Fullscreen :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
Set the `fullscreen` prop to `true` to enable it.
::component-example
#default
:modal-example-fullscreen
#code
```vue
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<UModal v-model="isOpen" fullscreen>
<UCard
:ui="{
base: 'h-full flex flex-col',
rounded: '',
divide: 'divide-y divide-gray-100 dark:divide-gray-800',
body: {
base: 'grow'
}
}"
>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Modal
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
</div>
</template>
<Placeholder class="h-full" />
</UCard>
</UModal>
</div>
</template>
```
::
## Props
:component-props

View File

@@ -3,7 +3,7 @@ description: Display a dialog that slides in from the edge of the screen.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/Slideover.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Slideover.vue
- label: 'Dialog'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/dialog'
@@ -124,7 +124,7 @@ const isOpen = ref(false)
```
::
### Prevent close :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
### Prevent close
Use the `prevent-close` prop to disable the outside click alongside the `esc` keyboard shortcut.

View File

@@ -3,7 +3,7 @@ description: Display a non-modal dialog that floats around a trigger element.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/Popover.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Popover.vue
- label: 'Popover'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/popover'
@@ -49,6 +49,12 @@ Use the `mode` prop to switch between `click` and `hover` modes.
```
::
## Slots
### `panel`
Use the `#panel` slot to fill the content of the panel.
## Props
:component-props

View File

@@ -3,7 +3,7 @@ description: Display content that appears on hover next to an element.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/Tooltip.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Tooltip.vue
---
## Usage
@@ -21,6 +21,22 @@ links:
```
::
## Slots
### `text`
Use the `#text` slot to override the content of the text.
::component-card
---
slots:
text: <span class="italic">Hello World!</span>
---
#text
[Hello World!]{.italic}
::
## Props
:component-props

View File

@@ -1,9 +1,10 @@
---
title: ContextMenu
description: Display a menu that appears on right click.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/ContextMenu.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/ContextMenu.vue
---
## Usage

View File

@@ -3,7 +3,7 @@ description: Display a toast notification in your app.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/Notification.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Notification.vue
---
## Usage
@@ -109,7 +109,7 @@ props:
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}` or change it globally in `ui.notification.default.icon`.
::component-card
---
@@ -316,7 +316,7 @@ excludedProps:
## Slots
### `title` / `description` :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
### `title` / `description`
Use the `#title` and `#description` slots to customize the Notification.

View File

@@ -3,7 +3,7 @@ description: Display a card for content with a header, body and footer.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/layout/Card.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/layout/Card.vue
---
## Usage
@@ -26,6 +26,16 @@ links:
```
::
## Slots
### `header`
Use the `#header` slot to fill the header.
### `footer`
Use the `#footer` slot to fill the footer.
## Props
:component-props

View File

@@ -3,7 +3,7 @@ description: A container lets you center and constrain the width of your content
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/layout/Container.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/layout/Container.vue
---
## Usage

View File

@@ -3,7 +3,7 @@ description: Display a placeholder while content is loading.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/layout/Skeleton.vue
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/layout/Skeleton.vue
---
## Usage

91
docs/content/index.yml Normal file
View File

@@ -0,0 +1,91 @@
navigation: false
title: 'Nuxt UI: Fully styled and customizable components for Nuxt'
description: 'It provides everything related to UI when building your Nuxt app. This includes components, icons, colors, dark mode but also keyboard shortcuts. Built with Headless UI and Tailwind CSS, published under MIT License.'
hero:
title: 'A <span class="text-primary">UI Library</span> for<br class="hidden lg:block"> Modern Web Apps'
description: 'Nuxt UI simplifies the creation of stunning and responsive web applications with its<br class="hidden lg:block"> comprehensive collection of fully styled and customizable UI components designed for Nuxt.'
sections:
- slot: demo
class: 'hidden lg:block dark:bg-gradient-to-b from-gray-900 to-gray-950/50 !pt-0'
- title: Everything you expect from a<br class="hidden lg:block"> <span class="text-primary">UI component library</span>
slot: features
class: 'dark:bg-gradient-to-b from-gray-900 to-gray-950/50 dark:lg:bg-none dark:lg:bg-gray-950/50'
features:
- title: Color Palette
description: 'Choose a primary and a gray color from your Tailwind CSS color palette. Components will be styled accordingly.'
icon: i-heroicons-swatch
to: /getting-started/theming#colors
class: 'col-span-7 row-span-3'
image: /illustrations/color-palette
orientation: 'horizontal'
- title: Fully Customizable
description: 'Change the style of any component in your App Config or customize them specifically through the ui prop.'
icon: i-heroicons-wrench-screwdriver
to: /getting-started/theming#components
image: /illustrations/fully-customizable
class: 'col-span-5 row-span-5 lg:mb-10'
orientation: 'vertical'
- title: Icons
description: 'Choose any of the 100k+ icons from the most popular icon libraries with the Icon component or the icon prop.'
icon: i-heroicons-face-smile
to: /getting-started/theming#icons
image: /illustrations/icon-library
class: 'col-span-7 row-span-3'
orientation: 'horizontal'
- title: Light & Dark
description: 'Every component is designed with dark mode in mind. Works out of the box with @nuxtjs/color-mode.'
to: /getting-started/theming#dark-mode
icon: i-heroicons-moon
image: /illustrations/dark-mode
class: 'col-span-5 row-span-5 lg:-mt-10 lg:mb-20'
orientation: 'vertical'
- title: Keyboard Shortcuts
description: 'Nuxt UI comes with a set of Vue composables to easily handle keyboard shortcuts in your app.'
icon: i-heroicons-computer-desktop
to: /getting-started/shortcuts
class: 'col-span-7 row-span-3'
image: /illustrations/keyboard-shortcuts
orientation: 'horizontal'
links:
- label: Learn more
to: /getting-started/theming
color: white
size: lg
trailingIcon: i-heroicons-arrow-right-20-solid
- title: 'A collection of <span class="text-primary">30+</span> components'
description: 'Get access to 30+ beautifully designed and fully customizable components built for Nuxt. These components<br class="hidden lg:block"> are updated regularly to ensure that you always have the latest features and functionalities.'
class: 'dark:bg-gradient-to-b from-gray-950/50 to-gray-900'
slot: categories
links:
- label: View all components
to: /elements/accordion
color: white
size: lg
trailingIcon: i-heroicons-arrow-right-20-solid
categories:
- label: Elements
to: /elements/dropdown
image: /illustrations/elements
badge: 9
- label: Forms
to: /forms/form
image: /illustrations/forms
badge: 10
- label: Data
to: /data/table
image: /illustrations/data
badge: 1
- label: Navigation
to: /navigation/command-palette
image: /illustrations/navigation
badge: 4
- label: Overlays
to: /overlays/modal
image: /illustrations/overlays
badge: 6
- label: Layout
to: /layout/card
image: /illustrations/layout
badge: 3
cta:
title: Trusted and supported by our<br class="hidden lg:block"> amazing community

57
docs/error.vue Normal file
View File

@@ -0,0 +1,57 @@
<template>
<div>
<Header />
<UContainer>
<UMain>
<UPage>
<UPageError :error="error" />
</UPage>
</UMain>
</UContainer>
<ClientOnly>
<UDocsSearch :files="files" :navigation="navigation" />
</ClientOnly>
<UNotifications />
</div>
</template>
<script setup lang="ts">
import type { NuxtError } from '#app'
const { prefix, removePrefixFromNavigation, removePrefixFromFiles } = useContentSource()
useSeoMeta({
title: 'Page not found',
description: 'We are sorry but this page could not be found.'
})
defineProps<{
error: NuxtError
}>()
const { data: navigation } = await useLazyAsyncData('navigation', () => fetchContentNavigation(), {
default: () => [],
transform: (navigation) => {
navigation = navigation.find(link => link._path === prefix.value)?.children || []
return prefix.value === '/main' ? removePrefixFromNavigation(navigation) : navigation
}
})
const { data: files } = await useLazyAsyncData('files', () => queryContent().where({ _type: 'markdown', navigation: { $ne: false } }).find(), {
default: () => [],
transform: (files) => {
files = files.filter(file => file._path.startsWith(prefix.value))
return prefix.value === '/main' ? removePrefixFromFiles(files) : files
}
})
// Provide
provide('navigation', navigation)
provide('files', files)
</script>

5
docs/layouts/default.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<div>
<slot />
</div>
</template>

41
docs/layouts/docs.vue Normal file
View File

@@ -0,0 +1,41 @@
<template>
<UMain>
<UContainer>
<UPage>
<template #left>
<UAside :links="anchors">
<BranchSelect />
<UNavigationTree :links="mapContentNavigation(navigation)" />
</UAside>
</template>
<slot />
</UPage>
</UContainer>
</UMain>
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
const { mapContentNavigation } = useElementsHelpers()
const navigation = inject<NavItem[]>('navigation')
const anchors = [{
label: 'Documentation',
icon: 'i-heroicons-book-open-solid',
to: '/getting-started'
}, {
label: 'Playground',
icon: 'i-simple-icons-stackblitz',
to: 'https://stackblitz.com/edit/nuxt-ui?file=app.config.ts,app.vue',
target: '_blank'
}, {
label: 'Releases',
icon: 'i-heroicons-rocket-launch-solid',
to: 'https://github.com/nuxt/ui/releases',
target: '_blank'
}]
</script>

View File

@@ -7,10 +7,11 @@ import pkg from '../package.json'
const { resolve } = createResolver(import.meta.url)
export default defineNuxtConfig({
extends: '@nuxt-themes/ui-kit',
extends: process.env.NUXT_ELEMENTS_PATH || '@nuxthq/elements',
modules: [
'@nuxt/content',
'@nuxt/devtools',
'nuxt-og-image',
// '@nuxt/devtools',
// '@nuxthq/studio',
module,
'@nuxtjs/fontaine',
@@ -30,25 +31,46 @@ export default defineNuxtConfig({
icons: ['heroicons', 'simple-icons'],
safelistColors: excludeColors(colors)
},
googleFonts: {
families: {
Inter: [400, 500, 600, 700]
content: {
sources: {
// overwrite default source AKA `content` directory
content: {
prefix: '/dev',
driver: 'fs',
base: resolve('./content')
},
main: {
prefix: '/main',
driver: 'github',
repo: 'nuxt/ui',
branch: 'main',
dir: 'docs/content'
}
}
},
routeRules: {
'/': { redirect: '/getting-started', prerender: false }
fontMetrics: {
fonts: ['DM Sans']
},
googleFonts: {
display: 'swap',
download: true,
families: {
'DM+Sans': [400, 500, 600, 700]
}
},
nitro: {
prerender: {
routes: ['/getting-started']
routes: [
'/',
'/getting-started',
'/dev/getting-started',
'/api/search.json'
]
}
},
experimental: {
payloadExtraction: false
},
componentMeta: {
globalsOnly: true,
exclude: [resolve('./components'), resolve('@nuxt-themes/ui-kit/components')],
exclude: ['@nuxtjs/mdc', resolve('./components'), resolve('@nuxthq/elements/components')],
metaFields: {
props: true,
slots: false,
@@ -59,5 +81,15 @@ export default defineNuxtConfig({
typescript: {
strict: false,
includeWorkspace: true
},
hooks: {
// Related to https://github.com/nuxt/nuxt/pull/22558
'components:extend': (components) => {
components.forEach((component) => {
if (component.global) {
component.global = 'sync'
}
})
}
}
})

View File

@@ -1,29 +1,31 @@
{
"name": "@nuxthq/ui-docs",
"name": "@nuxt/ui-docs",
"private": true,
"dependencies": {
"@nuxthq/ui": "workspace:latest"
"@nuxt/ui": "workspace:latest"
},
"devDependencies": {
"@iconify-json/heroicons": "latest",
"@iconify-json/simple-icons": "latest",
"@nuxt-themes/ui-kit": "npm:@nuxt-themes/ui-kit-edge@0.0.1-28178970.ce8b67a",
"@nuxt/content": "^2.7.2",
"@nuxt/devtools": "^0.6.7",
"@nuxt/eslint-config": "^0.1.1",
"@nuxt/content": "^2.8.2",
"@nuxt/devtools": "^0.8.2",
"@nuxt/eslint-config": "^0.2.0",
"@nuxthq/elements": "npm:@nuxthq/elements-edge@0.0.1-28234841.b091775",
"@nuxthq/studio": "^0.13.4",
"@nuxtjs/fontaine": "^0.4.1",
"@nuxtjs/google-fonts": "^3.0.2",
"@nuxtjs/plausible": "^0.2.1",
"@vueuse/nuxt": "^10.2.1",
"eslint": "^8.45.0",
"nuxt": "^3.6.5",
"@vueuse/nuxt": "^10.4.1",
"eslint": "^8.48.0",
"joi": "^17.10.1",
"nuxt": "^3.7.1",
"nuxt-component-meta": "^0.5.3",
"nuxt-lodash": "^2.5.0",
"typescript": "^5.1.6",
"nuxt-og-image": "^2.0.25",
"typescript": "^5.2.2",
"ufo": "^1.3.0",
"v-calendar": "^3.0.3",
"yup": "^1.2.0",
"joi": "^17.9.2",
"zod": "^3.21.4"
"zod": "^3.22.2"
}
}

View File

@@ -1,49 +1,81 @@
<template>
<UPage v-if="page">
<UPageHeader v-bind="page" :headline="headline" />
<UPage>
<UPageHeader :title="page.title" :description="page.description" :links="page.links" :headline="headline" />
<UPageBody prose>
<ContentRenderer v-if="page && page.body" :value="page" />
<ContentRenderer v-if="page.body" :value="page" />
<UButton
:to="githubLink"
variant="link"
icon="i-heroicons-pencil-square"
label="Edit this page on GitHub"
:padded="false"
class="mt-12"
/>
<UDivider v-if="surround?.length" />
<hr v-if="surround?.length" class="border-gray-200 dark:border-gray-800 my-8">
<UDocsSurround :surround="surround" />
<Footer />
<UDocsSurround :surround="removePrefixFromFiles(surround)" />
</UPageBody>
<template v-if="page.body?.toc?.links?.length" #right>
<UDocsToc :links="page.body.toc.links" />
<UDocsToc :links="page.body.toc.links">
<template #bottom>
<div class="hidden lg:block space-y-6 !mt-6">
<UDivider v-if="page.body?.toc?.links?.length" dashed />
<UPageLinks title="Community" :links="links" />
</div>
</template>
</UDocsToc>
</template>
</UPage>
<UPageError v-else />
</template>
<script setup lang="ts">
const route = useRoute()
const { prefix, removePrefixFromFiles } = useContentSource()
const { findPageHeadline } = useElementsHelpers()
const { data: page } = await useAsyncData(`docs-${route.path}`, () => queryContent(route.path).findOne())
const { data: surround } = await useAsyncData(`docs-${route.path}-surround`, () => queryContent()
.where({ _extension: 'md', navigation: { $ne: false } })
.findSurround(route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
)
definePageMeta({
layout: 'docs'
})
if (process.server && !page.value) {
const event = useRequestEvent()
setResponseStatus(event, 404)
const path = computed(() => route.path.startsWith(prefix.value) ? route.path : `${prefix.value}${route.path}`)
const { data: page } = await useAsyncData(path.value, () => queryContent(path.value).findOne())
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found' })
}
useContentHead(page)
const { data: surround } = await useAsyncData(`${path.value}-surround`, () => {
return queryContent(prefix.value)
.where({ _extension: 'md', navigation: { $ne: false } })
.findSurround((path.value.endsWith('/') ? path.value.slice(0, -1) : path.value))
})
const githubLink = computed(() => `https://github.com/nuxtlabs/ui/edit/dev/docs/content/${page?.value?._file}`)
const headline = computed(() => page.value._dir?.title ? page.value._dir.title : useLowerCase(page.value._dir))
useSeoMeta({
titleTemplate: '%s - Nuxt UI',
title: page.value.title,
ogTitle: `${page.value.title} - Nuxt UI`,
description: page.value.description,
ogDescription: page.value.description
})
defineOgImage({
component: 'Docs',
title: page.value.title,
description: page.value.description
})
const headline = computed(() => findPageHeadline(page.value))
const links = computed(() => [{
icon: 'i-heroicons-pencil-square',
label: 'Edit this page',
to: `https://github.com/nuxt/ui/edit/dev/docs/content/${page?.value?._file}`,
target: '_blank'
}, {
icon: 'i-heroicons-star',
label: 'Star on GitHub',
to: 'https://github.com/nuxt/ui',
target: '_blank'
}, {
icon: 'i-heroicons-book-open',
label: 'Nuxt documentation',
to: 'https://nuxt.com',
target: '_blank'
}])
</script>

200
docs/pages/index.vue Normal file
View File

@@ -0,0 +1,200 @@
<template>
<div>
<ULandingHero v-bind="page.hero" :ui="{ base: 'relative z-[1]' }" class="mb-[calc(var(--header-height)*2)]">
<template #title>
<span v-html="page.hero?.title" />
</template>
<template #description>
<span v-html="page.hero?.description" />
</template>
<template #links>
<UButton label="Get Started" icon="i-heroicons-rocket-launch" size="lg" to="/getting-started/installation" />
<UInput
v-model="source"
color="gray"
readonly
autocomplete="off"
icon="i-heroicons-command-line"
input-class="select-none"
size="lg"
:ui="{ base: 'disabled:cursor-default', icon: { trailing: { pointer: '' } } }"
>
<template #trailing>
<UButton
aria-label="Copy Code"
:color="copied ? 'primary' : 'gray'"
variant="link"
size="2xs"
:icon="copied ? 'i-heroicons-clipboard-document-check' : 'i-heroicons-clipboard-document'"
@click="copy(source)"
/>
</template>
</UInput>
</template>
<ClientOnly>
<HomeTetris />
</ClientOnly>
</ULandingHero>
<ULandingSection v-for="(section, index) of page.sections" :key="index" v-bind="section">
<template v-if="section.title" #title>
<span v-html="section?.title" />
</template>
<template v-if="section.description" #description>
<span v-html="section.description" />
</template>
<template #demo>
<ClientOnly>
<HomeDemo v-if="lgAndLarger" />
</ClientOnly>
</template>
<template #features>
<ULandingGrid class="lg:-mb-20 lg:auto-rows-[3rem]">
<ULandingCard
v-for="(feature, subIndex) of section.features"
:key="subIndex"
v-bind="feature"
:ui="{
background: 'dark:bg-gray-900/50 dark:lg:bg-gradient-to-b from-gray-700/50 to-gray-950/50',
body: {
base: 'flex-1',
background: 'dark:bg-gray-800/50 dark:lg:bg-gray-900/50 backdrop-blur-lg'
}
}"
class="flex flex-col"
>
<div v-if="feature.image">
<UColorModeImage :light="`${feature.image}-light.svg`" :dark="`${feature.image}-dark.svg`" class="object-cover w-full" />
</div>
</ULandingCard>
</ULandingGrid>
</template>
<template #categories>
<UPageGrid class="lg:gap-16">
<NuxtLink
v-for="(category, subIndex) of section.categories"
:key="subIndex"
:to="category.to"
class="hover:bg-gradient-to-b hover:from-gray-200/50 dark:hover:from-gray-800/50 rounded-lg"
>
<UColorModeImage :light="`${category.image}-light.svg`" :dark="`${category.image}-dark.svg`" class="object-cover w-full" />
<div class="flex items-center justify-center gap-2 mt-1 mb-2">
<span class="font-semibold text-lg">{{ category.label }}</span>
<UBadge v-if="category.badge" :label="category.badge" variant="subtle" size="xs" />
</div>
</NuxtLink>
</UPageGrid>
</template>
</ULandingSection>
<ULandingSection class="!pt-0">
<ULandingCTA
align="left"
card
:ui="{
background: 'dark:bg-gradient-to-b from-gray-800 to-gray-900',
shadow: 'dark:shadow-2xl',
body: {
background: 'bg-gray-50/50 dark:bg-gray-900/50'
},
title: 'text-center lg:text-left',
links: 'justify-center lg:justify-start'
}"
>
<template #title>
<span v-html="page.cta.title" />
</template>
<template #links>
<UAvatarGroup :max="xlAndLarger ? 13 : lgAndLarger ? 10 : mdAndLarger ? 16 : 8" size="md" class="flex-wrap-reverse [&_span:first-child]:text-xs justify-center">
<UTooltip
v-for="(contributor, index) of module.contributors"
:key="index"
:text="contributor.username"
class="rounded-full"
:ui="{ background: 'bg-gray-50 dark:bg-gray-800/50' }"
:popper="{ offsetDistance: 16 }"
>
<UAvatar
:alt="contributor.username"
:src="`https://ipx.nuxt.com/s_40x40/gh_avatar/${contributor.username}`"
:srcset="`https://ipx.nuxt.com/s_80x80/gh_avatar/${contributor.username} 2x`"
class="lg:hover:scale-125 lg:hover:ring-2 lg:hover:ring-primary-500 dark:lg:hover:ring-primary-400 transition-transform"
size="md"
>
<NuxtLink :to="`https://github.com/${contributor.username}`" target="_blank" class="focus:outline-none" tabindex="-1">
<span class="absolute inset-0" aria-hidden="true" />
</NuxtLink>
</UAvatar>
</UTooltip>
</UAvatarGroup>
</template>
<div class="flex flex-col sm:flex-row items-center justify-center gap-8 lg:gap-16">
<NuxtLink class="text-center group" to="https://npmjs.org/package/@nuxt/ui" target="_blank">
<p class="text-6xl font-semibold text-gray-900 dark:text-white group-hover:text-primary-500 dark:group-hover:text-primary-400">
{{ format(module.stats.downloads) }}+
</p>
<p>monthly downloads</p>
</NuxtLink>
<NuxtLink class="text-center group" to="https://github.com/nuxt/ui" target="_blank">
<p class="text-6xl font-semibold text-gray-900 dark:text-white group-hover:text-primary-500 dark:group-hover:text-primary-400">
{{ format(module.stats.stars) }}+
</p>
<p>stars</p>
</NuxtLink>
</div>
</ULandingCTA>
</ULandingSection>
</div>
</template>
<script setup lang="ts">
import { pick } from 'lodash-es'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
const { data: page } = await useAsyncData('index', () => queryContent('/').findOne())
const { data: module } = await useFetch<{
stats: {
downloads: number
stars: number
}
contributors: {
username: string
}[]
}>('https://api.nuxt.com/modules/ui', {
transform: (module) => pick(module, ['stats', 'contributors'])
})
const source = ref('npm i @nuxt/ui')
const { copy, copied } = useClipboard({ source })
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greaterOrEqual('md')
const lgAndLarger = breakpoints.greaterOrEqual('lg')
const xlAndLarger = breakpoints.greaterOrEqual('xl')
useSeoMeta({
titleTemplate: '',
title: page.value.title,
ogTitle: page.value.title,
description: page.value.description,
ogDescription: page.value.description,
ogImage: 'https://ui.nuxt.com/social-card.png',
twitterImage: 'https://ui.nuxt.com/social-card.png'
})
const { format } = Intl.NumberFormat('en-GB', { notation: 'compact' })
</script>

View File

@@ -12,8 +12,15 @@ export default defineNuxtPlugin({
return `:root {
${Object.entries(primary || colors.green).map(([key, value]) => `--color-primary-${key}: ${hexToRgb(value)};`).join('\n')}
--color-primary-DEFAULT: var(--color-primary-500);
${Object.entries(gray || colors.cool).map(([key, value]) => `--color-gray-${key}: ${hexToRgb(value)};`).join('\n')}
}`
}
.dark {
--color-primary-DEFAULT: var(--color-primary-400);
}
`
})
if (process.client) {

View File

@@ -0,0 +1,79 @@
<svg width="363" height="152" viewBox="0 0 363 152" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M358 0C360.209 0 362 1.79086 362 4V13.8182L324 13.8182V4C324 1.79086 325.791 0 328 0L358 0Z" fill="#F8FAFC"/>
<rect x="362" y="13.8182" width="13.8182" height="38" transform="rotate(90 362 13.8182)" fill="#F1F5F9"/>
<rect x="362" y="27.6364" width="13.8182" height="38" transform="rotate(90 362 27.6364)" fill="#E2E8F0"/>
<rect x="362" y="41.4546" width="13.8182" height="38" transform="rotate(90 362 41.4546)" fill="#CBD5E1"/>
<rect x="362" y="55.2727" width="13.8182" height="38" transform="rotate(90 362 55.2727)" fill="#94A3B8"/>
<rect x="362" y="69.0909" width="13.8182" height="38" transform="rotate(90 362 69.0909)" fill="#64748B"/>
<rect x="362" y="82.9091" width="13.8182" height="38" transform="rotate(90 362 82.9091)" fill="#475569"/>
<rect x="362" y="96.7273" width="13.8182" height="38" transform="rotate(90 362 96.7273)" fill="#334155"/>
<rect x="362" y="110.545" width="13.8182" height="38" transform="rotate(90 362 110.545)" fill="#1E293B"/>
<rect x="362" y="124.364" width="13.8182" height="38" transform="rotate(90 362 124.364)" fill="#0F172A"/>
<path d="M362 138.182V148C362 150.209 360.209 152 358 152H328C325.791 152 324 150.209 324 148V138.182H362Z" fill="#020420"/>
<path d="M304 0C306.209 0 308 1.79086 308 4V13.8182L270 13.8182V4C270 1.79086 271.791 0 274 0L304 0Z" fill="#EEF2FF"/>
<rect x="308" y="13.8182" width="13.8182" height="38" transform="rotate(90 308 13.8182)" fill="#E0E7FF"/>
<rect x="308" y="27.6364" width="13.8182" height="38" transform="rotate(90 308 27.6364)" fill="#C7D2FE"/>
<rect x="308" y="41.4546" width="13.8182" height="38" transform="rotate(90 308 41.4546)" fill="#A5B4FC"/>
<rect x="308" y="55.2727" width="13.8182" height="38" transform="rotate(90 308 55.2727)" fill="#818CF8"/>
<rect x="308" y="69.0909" width="13.8182" height="38" transform="rotate(90 308 69.0909)" fill="#6366F1"/>
<rect x="308" y="82.9091" width="13.8182" height="38" transform="rotate(90 308 82.9091)" fill="#4F46E5"/>
<rect x="308" y="96.7273" width="13.8182" height="38" transform="rotate(90 308 96.7273)" fill="#4338CA"/>
<rect x="308" y="110.545" width="13.8182" height="38" transform="rotate(90 308 110.545)" fill="#3730A3"/>
<rect x="308" y="124.364" width="13.8182" height="38" transform="rotate(90 308 124.364)" fill="#312E81"/>
<path d="M308 138.182V148C308 150.209 306.209 152 304 152H274C271.791 152 270 150.209 270 148V138.182H308Z" fill="#1E1B4B"/>
<path d="M250 0C252.209 0 254 1.79086 254 4V13.8182L216 13.8182V4C216 1.79086 217.791 0 220 0L250 0Z" fill="#F5F3FF"/>
<rect x="254" y="13.8182" width="13.8182" height="38" transform="rotate(90 254 13.8182)" fill="#EDE9FE"/>
<rect x="254" y="27.6364" width="13.8182" height="38" transform="rotate(90 254 27.6364)" fill="#DDD6FE"/>
<rect x="254" y="41.4546" width="13.8182" height="38" transform="rotate(90 254 41.4546)" fill="#C4B5FD"/>
<rect x="254" y="55.2727" width="13.8182" height="38" transform="rotate(90 254 55.2727)" fill="#A78BFA"/>
<rect x="254" y="69.0909" width="13.8182" height="38" transform="rotate(90 254 69.0909)" fill="#8B5CF6"/>
<rect x="254" y="82.9091" width="13.8182" height="38" transform="rotate(90 254 82.9091)" fill="#7C3AED"/>
<rect x="254" y="96.7273" width="13.8182" height="38" transform="rotate(90 254 96.7273)" fill="#6D28D9"/>
<rect x="254" y="110.545" width="13.8182" height="38" transform="rotate(90 254 110.545)" fill="#5B21B6"/>
<rect x="254" y="124.364" width="13.8182" height="38" transform="rotate(90 254 124.364)" fill="#4C1D95"/>
<path d="M254 138.182V148C254 150.209 252.209 152 250 152H220C217.791 152 216 150.209 216 148V138.182H254Z" fill="#2E1065"/>
<path d="M34 0C36.2091 0 38 1.79086 38 4V13.8182L0 13.8182V4C0 1.79086 1.79086 0 4 0L34 0Z" fill="#FFF1F2"/>
<rect x="38" y="13.8182" width="13.8182" height="38" transform="rotate(90 38 13.8182)" fill="#FFE4E6"/>
<rect x="38" y="27.6364" width="13.8182" height="38" transform="rotate(90 38 27.6364)" fill="#FECDD3"/>
<rect x="38" y="41.4546" width="13.8182" height="38" transform="rotate(90 38 41.4546)" fill="#FDA4AF"/>
<rect x="38" y="55.2727" width="13.8182" height="38" transform="rotate(90 38 55.2727)" fill="#FB7185"/>
<rect x="38" y="69.0909" width="13.8182" height="38" transform="rotate(90 38 69.0909)" fill="#F43F5E"/>
<rect x="38" y="82.9091" width="13.8182" height="38" transform="rotate(90 38 82.9091)" fill="#E11D48"/>
<rect x="38" y="96.7273" width="13.8182" height="38" transform="rotate(90 38 96.7273)" fill="#BE123C"/>
<rect x="38" y="110.545" width="13.8182" height="38" transform="rotate(90 38 110.545)" fill="#9F1239"/>
<rect x="38" y="124.364" width="13.8182" height="38" transform="rotate(90 38 124.364)" fill="#881337"/>
<path d="M38 138.182V148C38 150.209 36.2091 152 34 152H4C1.79086 152 0 150.209 0 148V138.182H38Z" fill="#4C0519"/>
<path d="M88 0C90.2091 0 92 1.79086 92 4V13.8182L54 13.8182V4C54 1.79086 55.7909 0 58 0L88 0Z" fill="#F0F9FF"/>
<rect x="92" y="13.8182" width="13.8182" height="38" transform="rotate(90 92 13.8182)" fill="#E0F2FE"/>
<rect x="92" y="27.6364" width="13.8182" height="38" transform="rotate(90 92 27.6364)" fill="#BAE6FD"/>
<rect x="92" y="41.4546" width="13.8182" height="38" transform="rotate(90 92 41.4546)" fill="#7DD3FC"/>
<rect x="92" y="55.2727" width="13.8182" height="38" transform="rotate(90 92 55.2727)" fill="#38BDF8"/>
<rect x="92" y="69.0909" width="13.8182" height="38" transform="rotate(90 92 69.0909)" fill="#0EA5E9"/>
<rect x="92" y="82.9091" width="13.8182" height="38" transform="rotate(90 92 82.9091)" fill="#0284C7"/>
<rect x="92" y="96.7273" width="13.8182" height="38" transform="rotate(90 92 96.7273)" fill="#0369A1"/>
<rect x="92" y="110.545" width="13.8182" height="38" transform="rotate(90 92 110.545)" fill="#075985"/>
<rect x="92" y="124.364" width="13.8182" height="38" transform="rotate(90 92 124.364)" fill="#0C4A6E"/>
<path d="M92 138.182V148C92 150.209 90.2091 152 88 152H58C55.7909 152 54 150.209 54 148V138.182H92Z" fill="#082F49"/>
<path d="M142 0C144.209 0 146 1.79086 146 4V13.8182L108 13.8182V4C108 1.79086 109.791 0 112 0L142 0Z" fill="#F0FDFA"/>
<rect x="146" y="13.8182" width="13.8182" height="38" transform="rotate(90 146 13.8182)" fill="#CCFBF1"/>
<rect x="146" y="27.6364" width="13.8182" height="38" transform="rotate(90 146 27.6364)" fill="#99F6E4"/>
<rect x="146" y="41.4546" width="13.8182" height="38" transform="rotate(90 146 41.4546)" fill="#5EEAD4"/>
<rect x="146" y="55.2727" width="13.8182" height="38" transform="rotate(90 146 55.2727)" fill="#2DD4BF"/>
<rect x="146" y="69.0909" width="13.8182" height="38" transform="rotate(90 146 69.0909)" fill="#14B8A6"/>
<rect x="146" y="82.9091" width="13.8182" height="38" transform="rotate(90 146 82.9091)" fill="#0D9488"/>
<rect x="146" y="96.7273" width="13.8182" height="38" transform="rotate(90 146 96.7273)" fill="#0F766E"/>
<rect x="146" y="110.545" width="13.8182" height="38" transform="rotate(90 146 110.545)" fill="#115E59"/>
<rect x="146" y="124.364" width="13.8182" height="38" transform="rotate(90 146 124.364)" fill="#134E4A"/>
<path d="M146 138.182V148C146 150.209 144.209 152 142 152H112C109.791 152 108 150.209 108 148V138.182H146Z" fill="#042F2E"/>
<path d="M196 0C198.209 0 200 1.79086 200 4V13.8182L162 13.8182V4C162 1.79086 163.791 0 166 0L196 0Z" fill="#F0FDF4"/>
<rect x="200" y="13.8182" width="13.8182" height="38" transform="rotate(90 200 13.8182)" fill="#DCFCE7"/>
<rect x="200" y="27.6364" width="13.8182" height="38" transform="rotate(90 200 27.6364)" fill="#BBF7D0"/>
<rect x="200" y="41.4546" width="13.8182" height="38" transform="rotate(90 200 41.4546)" fill="#86EFAC"/>
<rect x="200" y="55.2727" width="13.8182" height="38" transform="rotate(90 200 55.2727)" fill="#4ADE80"/>
<rect x="200" y="69.0909" width="13.8182" height="38" transform="rotate(90 200 69.0909)" fill="#22C55E"/>
<rect x="200" y="82.9091" width="13.8182" height="38" transform="rotate(90 200 82.9091)" fill="#16A34A"/>
<rect x="200" y="96.7273" width="13.8182" height="38" transform="rotate(90 200 96.7273)" fill="#15803D"/>
<rect x="200" y="110.545" width="13.8182" height="38" transform="rotate(90 200 110.545)" fill="#166534"/>
<rect x="200" y="124.364" width="13.8182" height="38" transform="rotate(90 200 124.364)" fill="#14532D"/>
<path d="M200 138.182V148C200 150.209 198.209 152 196 152H166C163.791 152 162 150.209 162 148V138.182H200Z" fill="#052E16"/>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1,79 @@
<svg width="363" height="152" viewBox="0 0 363 152" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M358 0C360.209 0 362 1.79086 362 4V13.8182L324 13.8182V4C324 1.79086 325.791 0 328 0L358 0Z" fill="#F8FAFC"/>
<rect x="362" y="13.8182" width="13.8182" height="38" transform="rotate(90 362 13.8182)" fill="#F1F5F9"/>
<rect x="362" y="27.6364" width="13.8182" height="38" transform="rotate(90 362 27.6364)" fill="#E2E8F0"/>
<rect x="362" y="41.4545" width="13.8182" height="38" transform="rotate(90 362 41.4545)" fill="#CBD5E1"/>
<rect x="362" y="55.2727" width="13.8182" height="38" transform="rotate(90 362 55.2727)" fill="#94A3B8"/>
<rect x="362" y="69.0909" width="13.8182" height="38" transform="rotate(90 362 69.0909)" fill="#64748B"/>
<rect x="362" y="82.9091" width="13.8182" height="38" transform="rotate(90 362 82.9091)" fill="#475569"/>
<rect x="362" y="96.7273" width="13.8182" height="38" transform="rotate(90 362 96.7273)" fill="#334155"/>
<rect x="362" y="110.545" width="13.8182" height="38" transform="rotate(90 362 110.545)" fill="#1E293B"/>
<rect x="362" y="124.364" width="13.8182" height="38" transform="rotate(90 362 124.364)" fill="#0F172A"/>
<path d="M362 138.182V148C362 150.209 360.209 152 358 152H328C325.791 152 324 150.209 324 148V138.182H362Z" fill="#020420"/>
<path d="M304 0C306.209 0 308 1.79086 308 4V13.8182L270 13.8182V4C270 1.79086 271.791 0 274 0L304 0Z" fill="#EEF2FF"/>
<rect x="308" y="13.8182" width="13.8182" height="38" transform="rotate(90 308 13.8182)" fill="#E0E7FF"/>
<rect x="308" y="27.6364" width="13.8182" height="38" transform="rotate(90 308 27.6364)" fill="#C7D2FE"/>
<rect x="308" y="41.4545" width="13.8182" height="38" transform="rotate(90 308 41.4545)" fill="#A5B4FC"/>
<rect x="308" y="55.2727" width="13.8182" height="38" transform="rotate(90 308 55.2727)" fill="#818CF8"/>
<rect x="308" y="69.0909" width="13.8182" height="38" transform="rotate(90 308 69.0909)" fill="#6366F1"/>
<rect x="308" y="82.9091" width="13.8182" height="38" transform="rotate(90 308 82.9091)" fill="#4F46E5"/>
<rect x="308" y="96.7273" width="13.8182" height="38" transform="rotate(90 308 96.7273)" fill="#4338CA"/>
<rect x="308" y="110.545" width="13.8182" height="38" transform="rotate(90 308 110.545)" fill="#3730A3"/>
<rect x="308" y="124.364" width="13.8182" height="38" transform="rotate(90 308 124.364)" fill="#312E81"/>
<path d="M308 138.182V148C308 150.209 306.209 152 304 152H274C271.791 152 270 150.209 270 148V138.182H308Z" fill="#1E1B4B"/>
<path d="M250 0C252.209 0 254 1.79086 254 4V13.8182L216 13.8182V4C216 1.79086 217.791 0 220 0L250 0Z" fill="#F5F3FF"/>
<rect x="254" y="13.8182" width="13.8182" height="38" transform="rotate(90 254 13.8182)" fill="#EDE9FE"/>
<rect x="254" y="27.6364" width="13.8182" height="38" transform="rotate(90 254 27.6364)" fill="#DDD6FE"/>
<rect x="254" y="41.4545" width="13.8182" height="38" transform="rotate(90 254 41.4545)" fill="#C4B5FD"/>
<rect x="254" y="55.2727" width="13.8182" height="38" transform="rotate(90 254 55.2727)" fill="#A78BFA"/>
<rect x="254" y="69.0909" width="13.8182" height="38" transform="rotate(90 254 69.0909)" fill="#8B5CF6"/>
<rect x="254" y="82.9091" width="13.8182" height="38" transform="rotate(90 254 82.9091)" fill="#7C3AED"/>
<rect x="254" y="96.7273" width="13.8182" height="38" transform="rotate(90 254 96.7273)" fill="#6D28D9"/>
<rect x="254" y="110.545" width="13.8182" height="38" transform="rotate(90 254 110.545)" fill="#5B21B6"/>
<rect x="254" y="124.364" width="13.8182" height="38" transform="rotate(90 254 124.364)" fill="#4C1D95"/>
<path d="M254 138.182V148C254 150.209 252.209 152 250 152H220C217.791 152 216 150.209 216 148V138.182H254Z" fill="#2E1065"/>
<path d="M34 0C36.2091 0 38 1.79086 38 4V13.8182L0 13.8182V4C0 1.79086 1.79086 0 4 0L34 0Z" fill="#FFF1F2"/>
<rect x="38" y="13.8182" width="13.8182" height="38" transform="rotate(90 38 13.8182)" fill="#FFE4E6"/>
<rect x="38" y="27.6364" width="13.8182" height="38" transform="rotate(90 38 27.6364)" fill="#FECDD3"/>
<rect x="38" y="41.4545" width="13.8182" height="38" transform="rotate(90 38 41.4545)" fill="#FDA4AF"/>
<rect x="38" y="55.2727" width="13.8182" height="38" transform="rotate(90 38 55.2727)" fill="#FB7185"/>
<rect x="38" y="69.0909" width="13.8182" height="38" transform="rotate(90 38 69.0909)" fill="#F43F5E"/>
<rect x="38" y="82.9091" width="13.8182" height="38" transform="rotate(90 38 82.9091)" fill="#E11D48"/>
<rect x="38" y="96.7273" width="13.8182" height="38" transform="rotate(90 38 96.7273)" fill="#BE123C"/>
<rect x="38" y="110.545" width="13.8182" height="38" transform="rotate(90 38 110.545)" fill="#9F1239"/>
<rect x="38" y="124.364" width="13.8182" height="38" transform="rotate(90 38 124.364)" fill="#881337"/>
<path d="M38 138.182V148C38 150.209 36.2091 152 34 152H4C1.79086 152 0 150.209 0 148V138.182H38Z" fill="#4C0519"/>
<path d="M88 0C90.2091 0 92 1.79086 92 4V13.8182L54 13.8182V4C54 1.79086 55.7909 0 58 0L88 0Z" fill="#F0F9FF"/>
<rect x="92" y="13.8182" width="13.8182" height="38" transform="rotate(90 92 13.8182)" fill="#E0F2FE"/>
<rect x="92" y="27.6364" width="13.8182" height="38" transform="rotate(90 92 27.6364)" fill="#BAE6FD"/>
<rect x="92" y="41.4545" width="13.8182" height="38" transform="rotate(90 92 41.4545)" fill="#7DD3FC"/>
<rect x="92" y="55.2727" width="13.8182" height="38" transform="rotate(90 92 55.2727)" fill="#38BDF8"/>
<rect x="92" y="69.0909" width="13.8182" height="38" transform="rotate(90 92 69.0909)" fill="#0EA5E9"/>
<rect x="92" y="82.9091" width="13.8182" height="38" transform="rotate(90 92 82.9091)" fill="#0284C7"/>
<rect x="92" y="96.7273" width="13.8182" height="38" transform="rotate(90 92 96.7273)" fill="#0369A1"/>
<rect x="92" y="110.545" width="13.8182" height="38" transform="rotate(90 92 110.545)" fill="#075985"/>
<rect x="92" y="124.364" width="13.8182" height="38" transform="rotate(90 92 124.364)" fill="#0C4A6E"/>
<path d="M92 138.182V148C92 150.209 90.2091 152 88 152H58C55.7909 152 54 150.209 54 148V138.182H92Z" fill="#082F49"/>
<path d="M142 0C144.209 0 146 1.79086 146 4V13.8182L108 13.8182V4C108 1.79086 109.791 0 112 0L142 0Z" fill="#F0FDFA"/>
<rect x="146" y="13.8182" width="13.8182" height="38" transform="rotate(90 146 13.8182)" fill="#CCFBF1"/>
<rect x="146" y="27.6364" width="13.8182" height="38" transform="rotate(90 146 27.6364)" fill="#99F6E4"/>
<rect x="146" y="41.4545" width="13.8182" height="38" transform="rotate(90 146 41.4545)" fill="#5EEAD4"/>
<rect x="146" y="55.2727" width="13.8182" height="38" transform="rotate(90 146 55.2727)" fill="#2DD4BF"/>
<rect x="146" y="69.0909" width="13.8182" height="38" transform="rotate(90 146 69.0909)" fill="#14B8A6"/>
<rect x="146" y="82.9091" width="13.8182" height="38" transform="rotate(90 146 82.9091)" fill="#0D9488"/>
<rect x="146" y="96.7273" width="13.8182" height="38" transform="rotate(90 146 96.7273)" fill="#0F766E"/>
<rect x="146" y="110.545" width="13.8182" height="38" transform="rotate(90 146 110.545)" fill="#115E59"/>
<rect x="146" y="124.364" width="13.8182" height="38" transform="rotate(90 146 124.364)" fill="#134E4A"/>
<path d="M146 138.182V148C146 150.209 144.209 152 142 152H112C109.791 152 108 150.209 108 148V138.182H146Z" fill="#042F2E"/>
<path d="M196 0C198.209 0 200 1.79086 200 4V13.8182L162 13.8182V4C162 1.79086 163.791 0 166 0L196 0Z" fill="#F0FDF4"/>
<rect x="200" y="13.8182" width="13.8182" height="38" transform="rotate(90 200 13.8182)" fill="#DCFCE7"/>
<rect x="200" y="27.6364" width="13.8182" height="38" transform="rotate(90 200 27.6364)" fill="#BBF7D0"/>
<rect x="200" y="41.4545" width="13.8182" height="38" transform="rotate(90 200 41.4545)" fill="#86EFAC"/>
<rect x="200" y="55.2727" width="13.8182" height="38" transform="rotate(90 200 55.2727)" fill="#4ADE80"/>
<rect x="200" y="69.0909" width="13.8182" height="38" transform="rotate(90 200 69.0909)" fill="#22C55E"/>
<rect x="200" y="82.9091" width="13.8182" height="38" transform="rotate(90 200 82.9091)" fill="#16A34A"/>
<rect x="200" y="96.7273" width="13.8182" height="38" transform="rotate(90 200 96.7273)" fill="#15803D"/>
<rect x="200" y="110.545" width="13.8182" height="38" transform="rotate(90 200 110.545)" fill="#166534"/>
<rect x="200" y="124.364" width="13.8182" height="38" transform="rotate(90 200 124.364)" fill="#14532D"/>
<path d="M200 138.182V148C200 150.209 198.209 152 196 152H166C163.791 152 162 150.209 162 148V138.182H200Z" fill="#052E16"/>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 128 KiB

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