Compare commits

...

123 Commits

Author SHA1 Message Date
Benjamin Canac
635b0f41e2 chore(release): v2.18.0 2024-07-25 10:44:46 +02:00
Benjamin Canac
5db18c0056 fix(Accordion): truncate buttons
Resolves #1909
2024-07-23 16:23:14 +02:00
Benjamin Canac
c3122f776d feat(Checkbox/Radio/RadioGroup): add help slot
Resolves #1957
2024-07-23 16:16:57 +02:00
Maxime Pauvert
b264ad2ebd feat(CommandPalette): handle static groups (#1458)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-07-23 16:05:20 +02:00
Benjamin Canac
f374b14dba docs: update badges 2024-07-23 15:46:14 +02:00
Everton
7155318029 feat(Table): expand row (#1036)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-07-23 15:43:39 +02:00
Benjamin Canac
c39c770a70 chore(Table): columns types 2024-07-23 14:55:06 +02:00
Cardona Simon
d18477def5 feat(RadioGroup): add selected to label slot props (#1587) 2024-07-23 14:54:18 +02:00
Alexander Trost
748e49175d feat(Table): handle rowClass property in columns (#1632)
Signed-off-by: Alexander Trost <galexrt@googlemail.com>
2024-07-23 14:44:33 +02:00
chenying
f74f1df6ca fix(Carousel): remove mix-blend-overlay on indicators (#1714)
Co-authored-by: chenying <chenying@addcn.com>
2024-07-23 14:39:16 +02:00
Benjamin Canac
d2d37e4093 playground(nuxt.config): add compatibilityDate 2024-07-23 14:36:58 +02:00
Benjamin Canac
b57676c1a5 docs(nuxt.config): add compatibilityDate 2024-07-23 14:36:31 +02:00
Abel Derderian
e8eb3941ad feat(Tabs): handle icon in items (#1798)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-07-23 14:36:03 +02:00
Benjamin Canac
839bf72c61 chore(deps): remove typescript 2024-07-23 14:19:30 +02:00
Benjamin Canac
fcf46d7171 chore(CommandPalette): improve fuse.js import types .ts file 2024-07-23 14:17:58 +02:00
Benjamin Canac
8c531615ef chore(types): rename .d.ts to .ts 2024-07-23 14:08:57 +02:00
Benjamin Canac
51e6cb8417 chore(deps): remove @nuxt/module-builder resolution 2024-07-23 14:08:37 +02:00
Benjamin Canac
2443fd70eb docs: typecheck 2024-07-23 13:22:25 +02:00
Benjamin Canac
4974280d82 docs(deps): update @nuxt/ui-pro 2024-07-23 12:14:07 +02:00
Benjamin Canac
b7d90f78b8 chore(deps): update vue-tsc resolution 2024-07-23 12:03:51 +02:00
Benjamin Canac
b2e0566c16 docs: typecheck 2024-07-23 11:51:02 +02:00
Thiago Petherson
64e8a87073 docs(notification): improve example (#1973) 2024-07-23 11:07:02 +02:00
Benjamin Canac
720c44dd5e fix(Link): allow ariaLabel to be picked
Resolves #1934
2024-07-22 12:24:09 +02:00
Matej Černý
6b216cab1b feat(SelectMenu): add selected to label / leading / trailing slots props (#1349)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-07-22 12:12:32 +02:00
Benjamin Canac
82c8717fbf chore(deps): dedupe 2024-07-22 11:15:06 +02:00
renovate[bot]
d1c80861a1 chore(deps): update all non-major dependencies (#1969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-21 15:54:10 +02:00
renovate[bot]
2ac31c0173 chore(deps): update nuxt framework to ^3.12.4 (#1971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-21 12:16:26 +02:00
renovate[bot]
77f0156b43 chore(deps): update dependency @octokit/rest to v21 (#1894)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 13:51:55 +02:00
Vincent Ratier
7e974b55d7 feat(SelectMenu): handle function in showCreateOptionWhen prop (#1853)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-07-18 13:51:01 +02:00
Benjamin Canac
6aaf12b9af chore(github): update stale workflow 2024-07-18 10:52:30 +02:00
Benjamin Canac
4b397c0f7e chore(github): add stale workflow 2024-07-17 14:58:49 +02:00
renovate[bot]
80b2a60718 chore(deps): update all non-major dependencies (#1967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-17 14:37:48 +02:00
Benjamin Canac
6691311782 chore(github): update issue templates 2024-07-17 14:36:36 +02:00
Benjamin Canac
2c55fb6336 fix(Alert/Notification): missing margin on description
Resolves #1959
2024-07-16 17:10:09 +02:00
Benjamin Canac
e2881d3801 fix(InputMenu/SelectMenu): prevent double filter with async search
Resolves #1966
2024-07-16 15:10:37 +02:00
renovate[bot]
46e9b128da chore(deps): update all non-major dependencies (#1756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-07-16 14:45:13 +02:00
Benjamin Canac
53003fcd07 fix(Breadcrumb): use rotate on rtl icon 2024-07-10 17:48:42 +02:00
Benjamin Canac
2bb8360223 docs(deps): add nuxt-component-meta resolution 2024-07-10 17:48:42 +02:00
Juho Rutila
bce94db9fd feat(Skeleton): add as prop (#1955) 2024-07-10 13:58:01 +02:00
Adrien Autricque
cf482581f4 docs(form): typo (#1953)
Co-authored-by: Adrien AUTRICQUE <adrien.autricque@exotec.com>
2024-07-10 11:19:29 +02:00
Benjamin Canac
19cab36d73 chore(deps): update 2024-07-09 14:25:30 +02:00
Benjamin Canac
df2b5e2419 chore(renovate): ignore @nuxt/module-builder 2024-07-09 14:20:41 +02:00
Benjamin Canac
1334ec59b1 chore(renovate): ignore vue-tsc 2024-07-09 12:35:19 +02:00
Dave Stewart
3f8ea5dbde feat(module): improve app config types autocomplete (#1870)
Co-authored-by: Dave Stewart <dev@davestewart.co.uk>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-07-09 12:23:57 +02:00
renovate[bot]
c0ef69d314 chore(deps): update devdependency vitest to v2 (#1951)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 11:08:40 +02:00
Rihan
567045211a chore(deps): update pnpm/action-setup action to v4 (#1942)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-07-09 11:08:11 +02:00
Benjamin Canac
9e12e9519e docs: update headlessui.com broken urls 2024-07-05 16:14:06 +02:00
Benjamin Canac
67e6a4a565 chore(renovate): ignore valibot30 / valibot31 deps 2024-07-03 19:29:31 +02:00
renovate[bot]
2dcf70cf3c chore(deps): update nuxt framework to ^3.12.3 (#1938)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-03 18:07:50 +02:00
Adrien Figueiredo
8eca5a0d62 fix(RadioGroup): allow boolean in modelValue prop (#1913)
Co-authored-by: Adrien Figueiredo <>
2024-07-01 11:02:36 +02:00
Daniel Roe
3e9c96bb7a docs: add site.url (#1924) 2024-06-28 15:24:00 +02:00
Christoph Biering
cb36094dbe chore(deps): update @nuxtjs/color-mode to v3.4.2 (#1922)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-06-27 12:31:34 +02:00
Fabian Hiller
839a6029e7 chore(deps): update valibot to v0.33.0 (#1914) 2024-06-27 11:57:34 +02:00
Benjamin Canac
4c0a3d8dc0 docs(icon): put back component page 2024-06-25 11:44:51 +02:00
Benjamin Canac
c904604c23 feat(Icon)!: migrate from @egoist/tailwindcss-icons to new @nuxt/icon (#1789) 2024-06-24 12:46:41 +02:00
Benjamin Canac
bfa2e707d8 chore(deps): update pnpm 2024-06-24 12:20:26 +02:00
Benjamin Canac
f16291d290 docs(deps): update @nuxt/content 2024-06-24 11:37:34 +02:00
Neil Richter
abd13f1f8f fix(Progress): pass down attrs to <progress> to improve accessibility (#1881) 2024-06-24 10:57:01 +02:00
John Tanzer
0a47a2b5f2 chore(eslint): add no-trailing-spaces rule (#1906) 2024-06-24 10:55:50 +02:00
Neil Richter
99c52e5008 fix(FormGroup): don't check for error slot so help slot can render (#1888) 2024-06-24 10:54:14 +02:00
Benjamin Canac
4d5f250902 fix(InputMenu/SelectMenu): invalid label with value-attribute and async search
Resolves #1780
2024-06-19 12:19:15 +02:00
Romain Hamel
6b6b03d59f fix(ButtonGroup/FormGroup): pass default sizes to children (#1875) 2024-06-19 12:08:13 +02:00
Benjamin Canac
4ad904f83d docs(home): improve contributors animation 2024-06-18 22:13:10 +02:00
Benjamin Canac
c28ef0abb8 docs(home): improve community section (#1891) 2024-06-18 17:29:52 +02:00
Benjamin Canac
9c3ed4b78f chore(github): remove pnpm version with packageManger 2024-06-18 17:27:58 +02:00
muri
a1c116de7b docs(form): improve validation references (#1877) 2024-06-18 14:02:51 +02:00
Benjamin Canac
755c4d32b3 docs(deps): update @nuxt/ui-pro 2024-06-18 13:11:24 +02:00
Benjamin Canac
05c7836387 docs(deps): update nuxt-og-image to v3 2024-06-18 11:55:28 +02:00
renovate[bot]
e9d801c52a chore(deps): update nuxt framework to ^3.12.2 (#1884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-18 10:22:47 +02:00
Daniel Roe
7211e3a1eb chore: indicate compatibility with new v4 major (#1879)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-06-15 10:35:32 +02:00
Benjamin Canac
89d18f8f1d docs(deps): update @nuxt/ui-pro 2024-06-13 12:58:37 +02:00
Benjamin Canac
1e7470f531 docs: update badges 2024-06-13 11:29:39 +02:00
Benjamin Canac
b6736d1efd chore(release): v2.17.0 2024-06-13 11:24:50 +02:00
Benjamin Canac
f6e695ffc8 chore(deps): update 2024-06-13 11:15:33 +02:00
Benjamin Canac
e8898d15a6 fix(Alert/Notification): use div for description
Resolves #1551
2024-06-13 11:15:27 +02:00
Thibault Vlacich
f65aefb706 fix(Alert): base style not applied on icon (#1859)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-06-12 10:25:18 +02:00
Benjamin Canac
9b9ccdb59e fix(SelectMenu): wrong placeholder color when modelValue is an empty string
Resolves #1862
2024-06-12 10:11:16 +02:00
renovate[bot]
688232215d chore(deps): update nuxt framework to ^3.12.1 (#1861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-11 17:13:37 +02:00
Benjamin Canac
ebfb835033 fix(Breadcrumb): allow aria-current to be overrideable
Related to #1856
2024-06-11 12:27:44 +02:00
renovate[bot]
838d6c832f chore(deps): update nuxt framework to ^3.12.0 (#1860)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-11 12:15:16 +02:00
eduardo-faith
e7c2f7856c fix(Input): hide wrapper when type is hidden (#1797)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-06-11 11:58:49 +02:00
Fabian Hiller
1d5bd89d58 feat(Form): update and migrate valibot to v0.31.0 (#1848)
Co-authored-by: Romain Hamel <romain@boilr.io>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-06-11 11:40:44 +02:00
Benjamin Canac
6c124bb1ac fix(Select): remove defaults for value and text
Resolves #1702
2024-06-06 10:51:34 +02:00
Benjamin Canac
49174b7628 chore(deps): update @​egoist/tailwindcss-icons
Resolves #1840
2024-06-06 10:44:50 +02:00
Vitta
50ad14f9df feat(Slideover): handle top and bottom side (#1834) 2024-06-05 10:30:45 +02:00
Vitta
6e2678d1d8 feat(Tabs): add content prop to avoid the render of the HTML markup (#1831)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-06-04 10:23:17 +02:00
Khaled Oghli
831c560a96 docs(slideover): add close button in some examples for mobile (#1827)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-06-03 15:30:45 +02:00
Romain Hamel
06990beabf fix(Form): maintain other errors when using setErrors with a path (#1818)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-06-03 13:59:26 +02:00
networdai
3ebff4d133 feat(Notification): allow ring customization with {color} (#1830)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-06-03 11:45:36 +02:00
Khaled Oghli
d66cfa9d7d docs(table): ensure scroll and pagination visibility on mobile (#1828)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-06-03 11:44:46 +02:00
Benjamin Canac
75c0d9e31f chore(deps): update 2024-06-03 11:40:39 +02:00
Benjamin Canac
6033872ef8 chore(deps): pin vue-tsc 2024-06-03 11:04:18 +02:00
Benjamin Canac
838cb7212a docs(alert): remove missing example 2024-06-03 11:00:42 +02:00
John Tanz
c8dd71c4f5 feat(Alert): add actions slot (#1785)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-05-28 12:29:10 +02:00
Mukund Shah
4f0d00f7a6 fix(Carousel): prevent mouse click when dragging (#1781) 2024-05-15 12:18:51 +02:00
Benjamin Canac
3b975634e8 chore(package): define packageManager 2024-05-14 15:13:11 +02:00
Benjamin Canac
249bbd49dc fix(CommandPalette): hide empty-state when null
Resolves #1787
2024-05-14 14:57:19 +02:00
Milos Dimitrijevic
3c1602af37 docs(form): fix link to /form-group (#1777) 2024-05-10 16:29:22 +02:00
Benjamin Canac
e1ca6e0cde chore(deps): update @headlessui/vue
Resolves #1760
2024-05-10 11:41:53 +02:00
Benjamin Canac
3b3bd16afe docs(deps): update @nuxt/ui-pro 2024-05-10 11:25:45 +02:00
Benjamin Canac
fab9cbebd8 docs(context-menu): add missing @vueuse/core imports
Resolves #1762
2024-05-10 11:10:00 +02:00
Benjamin Canac
581b470cc7 fix(Link): typo in exactHash type
Resolves #1767
2024-05-09 11:38:39 +02:00
Benjamin Canac
24d30cd1f3 chore(deps): pin @headlessui/vue to 1.7.20
Resolves #1760
2024-05-09 11:38:10 +02:00
Benjamin Canac
cc52bffccf chore(release): v2.16.0 2024-05-07 14:19:28 +02:00
renovate[bot]
eb2601d4da chore(deps): update all non-major dependencies (#1752)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-05-07 14:16:58 +02:00
renovate[bot]
f726b5f094 chore(deps): update all non-major dependencies (#1719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-05-06 17:00:48 +02:00
guylil
37ce62acb9 docs(FormGroup): incorrect icon in description slot example (#1749) 2024-05-06 12:22:48 +02:00
Romain Hamel
f97b728968 docs(Form): clarify when the @error event is triggered (#1747) 2024-05-06 10:39:05 +02:00
chenying
7e6ba78681 feat(InputMenu/SelectMenu): allow lazy search (#1705)
Co-authored-by: chenying <chenying@addcn.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-04-26 15:43:29 +02:00
Benjamin Canac
ed5c74dc17 fix(Input)!: redesign file type without absolute positioning (#1712) 2024-04-25 17:12:06 +02:00
Benjamin Canac
bb3ea40218 chore(deps): remove resolutions + update 2024-04-25 13:00:16 +02:00
Inesh Bose
821e15b696 feat(module): HMR support with @nuxtjs/tailwindcss (#1665)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-04-24 12:20:10 +02:00
Eugen Istoc
bd3fa8658f fix(Slideover): export and clean types (#1692)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-04-22 11:13:34 +02:00
Moritz
82d619b2a7 feat(useToast): allow clearing all notifications (#1695) 2024-04-22 11:01:48 +02:00
Neil Richter
8d9d9736ba feat(Pagination): allow using links for pagination buttons (#1682) 2024-04-18 12:38:48 +02:00
Neil Richter
3fca66857d feat(Table): allow providing a <caption> (#1680) 2024-04-16 16:39:52 +02:00
renovate[bot]
4853520eb3 chore(deps): update devdependency @nuxt/test-utils to ^3.12.1 (#1677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 11:11:35 +02:00
Benjamin Canac
5481dab53d fix(Breadcrumb): pass click event to ULink 2024-04-16 10:36:45 +02:00
Neil Richter
6f60fa9a98 fix(Table): provide aria-sort for sortable table headings (#1675) 2024-04-15 18:13:01 +02:00
Neil Richter
cba9ad78db fix(Notification): update timer when timeout prop changes (#1673) 2024-04-15 16:38:07 +02:00
Damian Głowala
bbc8f4e6ac chore(README): update installation section (#1659) 2024-04-15 10:28:51 +02:00
Neil Richter
ed3a3babdb feat(useToast): allow updating an existing notification (#1668) 2024-04-15 10:27:39 +02:00
renovate[bot]
4415d4111e chore(deps): update devdependency @nuxt/ui-pro to v1.1.0-28548740.d20325b (#1658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-12 19:12:53 +02:00
169 changed files with 13629 additions and 11011 deletions

View File

@@ -15,6 +15,7 @@ module.exports = {
'space-before-blocks': ['error', 'always'],
'space-infix-ops': ['error', { int32Hint: false }],
'no-multi-spaces': ['error', { ignoreEOLComments: true }],
'no-trailing-spaces': ['error'],
// Typescript
'@typescript-eslint/type-annotation-spacing': 'error',

View File

@@ -1,6 +1,6 @@
name: "🐛 Bug report"
description: Report a bug to help us improve the module.
labels: ["bug"]
labels: ["triage"]
body:
- type: markdown
attributes:

View File

@@ -4,5 +4,5 @@ contact_links:
url: https://ui.nuxt.com
about: Check the documentation for guides and examples.
- name: 📚 Discord
url: https://discord.com/channels/473401852243869706/1153996761426300948
about: Consider asking questions in the `#ui` channel.
url: https://go.nuxt.com/discord
about: Consider asking questions in the help channel.

View File

@@ -1,6 +1,6 @@
name: "🚀 Feature request"
description: Suggest an idea or enhancement for the module.
labels: ["enhancement"]
labels: ["triage"]
body:
- type: markdown
attributes:

View File

@@ -19,41 +19,26 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest] # macos-latest, windows-latest
node: [18]
node: [20]
env:
NUXT_GITHUB_TOKEN: ${{ secrets.NUXT_GITHUB_TOKEN }}
steps:
- uses: actions/setup-node@v4
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: checkout
uses: actions/checkout@master
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: dorny/paths-filter@v3
- name: Filter changes
uses: dorny/paths-filter@v3
id: changes
with:
filters: |

View File

@@ -12,39 +12,23 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest] # macos-latest, windows-latest
node: [18]
node: [20]
env:
NUXT_GITHUB_TOKEN: ${{ secrets.NUXT_GITHUB_TOKEN }}
steps:
- uses: actions/setup-node@v4
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: checkout
uses: actions/checkout@master
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
cache: pnpm
- name: Install dependencies
run: pnpm install

23
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: stale
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
exempt-issue-labels: triage,v3
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity.'
stale-issue-label: stale
stale-pr-label: stale
days-before-stale: 30
days-before-close: -1

View File

@@ -1,5 +1,89 @@
# Changelog
## [2.18.0](https://github.com/nuxt/ui/compare/v2.17.0...v2.18.0) (2024-07-25)
### ⚠ BREAKING CHANGES
* **Icon:** migrate from `@egoist/tailwindcss-icons` to new `@nuxt/icon` (#1789)
### Features
* **Checkbox/Radio/RadioGroup:** add `help` slot ([c3122f7](https://github.com/nuxt/ui/commit/c3122f776daa6d68f201f22c37e0084aac37ed06)), closes [#1957](https://github.com/nuxt/ui/issues/1957)
* **CommandPalette:** handle `static` groups ([#1458](https://github.com/nuxt/ui/issues/1458)) ([b264ad2](https://github.com/nuxt/ui/commit/b264ad2ebdc8d4ee4aab5c994df968025207021f))
* **Icon:** migrate from `@egoist/tailwindcss-icons` to new `@nuxt/icon` ([#1789](https://github.com/nuxt/ui/issues/1789)) ([c904604](https://github.com/nuxt/ui/commit/c904604c23987c2535e0e91e9c4fec50477f6b34))
* **module:** improve app config types autocomplete ([#1870](https://github.com/nuxt/ui/issues/1870)) ([3f8ea5d](https://github.com/nuxt/ui/commit/3f8ea5dbded7b6836495103739688905ff26fe22))
* **RadioGroup:** add `selected` to label slot props ([#1587](https://github.com/nuxt/ui/issues/1587)) ([d18477d](https://github.com/nuxt/ui/commit/d18477def58171d51bdb7d00e31e2807b2e7015b))
* **SelectMenu:** add selected to `label` / `leading` / `trailing` slots props ([#1349](https://github.com/nuxt/ui/issues/1349)) ([6b216ca](https://github.com/nuxt/ui/commit/6b216cab1ba3bb69cb317254dfd562ab020c5e92))
* **SelectMenu:** handle function in `showCreateOptionWhen` prop ([#1853](https://github.com/nuxt/ui/issues/1853)) ([7e974b5](https://github.com/nuxt/ui/commit/7e974b55d72b8ac0ab42ef722a2d1904c3e4e091))
* **Skeleton:** add `as` prop ([#1955](https://github.com/nuxt/ui/issues/1955)) ([bce94db](https://github.com/nuxt/ui/commit/bce94db9fdb2c29a4f2e5981e5dce49a44a4ac8a))
* **Table:** expand row ([#1036](https://github.com/nuxt/ui/issues/1036)) ([7155318](https://github.com/nuxt/ui/commit/71553180294c53024c28de9bbebf4ea69f616da7))
* **Table:** handle `rowClass` property in `columns` ([#1632](https://github.com/nuxt/ui/issues/1632)) ([748e491](https://github.com/nuxt/ui/commit/748e49175da37b85bd18d62a8455875990866d5b))
* **Tabs:** handle `icon` in items ([#1798](https://github.com/nuxt/ui/issues/1798)) ([e8eb394](https://github.com/nuxt/ui/commit/e8eb3941ad4c1c306ccbe9e11d979d5f6c808330))
### Bug Fixes
* **Accordion:** truncate buttons ([5db18c0](https://github.com/nuxt/ui/commit/5db18c00565f9d2bb9f2768c2de2ab291a55bcae)), closes [#1909](https://github.com/nuxt/ui/issues/1909)
* **Alert/Notification:** missing margin on description ([2c55fb6](https://github.com/nuxt/ui/commit/2c55fb63365ee7cc1e993ebd5aa5f83ddadcd26a)), closes [#1959](https://github.com/nuxt/ui/issues/1959)
* **Breadcrumb:** use `rotate` on rtl icon ([53003fc](https://github.com/nuxt/ui/commit/53003fcd07d67d13ada0759ff6c5cd3635fba0e3))
* **ButtonGroup/FormGroup:** pass default sizes to children ([#1875](https://github.com/nuxt/ui/issues/1875)) ([6b6b03d](https://github.com/nuxt/ui/commit/6b6b03d59f5ab3096b731c59d18a1085d25b5e8e))
* **Carousel:** remove `mix-blend-overlay` on indicators ([#1714](https://github.com/nuxt/ui/issues/1714)) ([f74f1df](https://github.com/nuxt/ui/commit/f74f1df6ca5f93e11e542245b611c1aa7c4b8308))
* **FormGroup:** don't check for `error` slot so `help` slot can render ([#1888](https://github.com/nuxt/ui/issues/1888)) ([99c52e5](https://github.com/nuxt/ui/commit/99c52e50082d5e99440894c7a077a17510f0de50))
* **InputMenu/SelectMenu:** invalid `label` with `value-attribute` and async search ([4d5f250](https://github.com/nuxt/ui/commit/4d5f2509022e4fb74fc268d5479f7cc8f0415040)), closes [#1780](https://github.com/nuxt/ui/issues/1780)
* **InputMenu/SelectMenu:** prevent double filter with async search ([e2881d3](https://github.com/nuxt/ui/commit/e2881d3801c54c49d66d41d4f0ba312a7b3ebce7)), closes [#1966](https://github.com/nuxt/ui/issues/1966)
* **Link:** allow `ariaLabel` to be picked ([720c44d](https://github.com/nuxt/ui/commit/720c44dd5ee90bb3b30aef32f01ff6eae1397aa4)), closes [#1934](https://github.com/nuxt/ui/issues/1934)
* **Progress:** pass down attrs to `<progress>` to improve accessibility ([#1881](https://github.com/nuxt/ui/issues/1881)) ([abd13f1](https://github.com/nuxt/ui/commit/abd13f1f8fd4c8b10069174534c5fdec6c83576e))
* **RadioGroup:** allow boolean in `modelValue` prop ([#1913](https://github.com/nuxt/ui/issues/1913)) ([8eca5a0](https://github.com/nuxt/ui/commit/8eca5a0d627e22f42350a060f09c4e44b6de422f))
## [2.17.0](https://github.com/nuxt/ui/compare/v2.16.0...v2.17.0) (2024-06-13)
### Features
* **Alert:** add `actions` slot ([#1785](https://github.com/nuxt/ui/issues/1785)) ([c8dd71c](https://github.com/nuxt/ui/commit/c8dd71c4f5a5239811b07b50f1dc802101af07d5))
* **Form:** update and migrate `valibot` to v0.31.0 ([#1848](https://github.com/nuxt/ui/issues/1848)) ([1d5bd89](https://github.com/nuxt/ui/commit/1d5bd89d5881163fc6dc917e138b9d8304dff6c4))
* **Notification:** allow ring customization with `{color}` ([#1830](https://github.com/nuxt/ui/issues/1830)) ([3ebff4d](https://github.com/nuxt/ui/commit/3ebff4d133372e339e2c4c439576e9e192b29cc3))
* **Slideover:** handle `top` and `bottom` side ([#1834](https://github.com/nuxt/ui/issues/1834)) ([50ad14f](https://github.com/nuxt/ui/commit/50ad14f9dffe4f76bef888cd10d30b417c75bca5))
* **Tabs:** add `content` prop to avoid the render of the HTML markup ([#1831](https://github.com/nuxt/ui/issues/1831)) ([6e2678d](https://github.com/nuxt/ui/commit/6e2678d1d8a498322eb3eff909ccbba55e40a2b7))
### Bug Fixes
* **Alert/Notification:** use `div` for description ([e8898d1](https://github.com/nuxt/ui/commit/e8898d15a667ba66e78828315e3cc4e92845cd3f)), closes [#1551](https://github.com/nuxt/ui/issues/1551)
* **Alert:** base style not applied on icon ([#1859](https://github.com/nuxt/ui/issues/1859)) ([f65aefb](https://github.com/nuxt/ui/commit/f65aefb7067c1c64c1355b5d699129e716ef1281))
* **Breadcrumb:** allow `aria-current` to be overrideable ([ebfb835](https://github.com/nuxt/ui/commit/ebfb8350339725c0a6f88c73f16bff01d61538c2)), closes [#1856](https://github.com/nuxt/ui/issues/1856)
* **Carousel:** prevent mouse click when dragging ([#1781](https://github.com/nuxt/ui/issues/1781)) ([4f0d00f](https://github.com/nuxt/ui/commit/4f0d00f7a6eebf05adceaf1e7c2869ad91949cf3))
* **CommandPalette:** hide `empty-state` when `null` ([249bbd4](https://github.com/nuxt/ui/commit/249bbd49dc8420603e8d561543d237abeb400908)), closes [#1787](https://github.com/nuxt/ui/issues/1787)
* **Form:** maintain other errors when using `setErrors` with a path ([#1818](https://github.com/nuxt/ui/issues/1818)) ([06990be](https://github.com/nuxt/ui/commit/06990beabf67f668322b4d3fb2ec93cc4f3bdcd4))
* **Input:** hide wrapper when type is `hidden` ([#1797](https://github.com/nuxt/ui/issues/1797)) ([e7c2f78](https://github.com/nuxt/ui/commit/e7c2f7856c05ed96f48c83d64d8e1d3f41ab58fe))
* **Link:** typo in `exactHash` type ([581b470](https://github.com/nuxt/ui/commit/581b470cc79c2315bb2d56e02a7c134a7861c616)), closes [#1767](https://github.com/nuxt/ui/issues/1767)
* **SelectMenu:** wrong placeholder color when `modelValue` is an empty string ([9b9ccdb](https://github.com/nuxt/ui/commit/9b9ccdb59e98fed096dd18809af646b10de46b9f)), closes [#1862](https://github.com/nuxt/ui/issues/1862)
* **Select:** remove defaults for `value` and `text` ([6c124bb](https://github.com/nuxt/ui/commit/6c124bb1ac2fef116161da56a3a8e5f92144ce3a)), closes [#1702](https://github.com/nuxt/ui/issues/1702)
## [2.16.0](https://github.com/nuxt/ui/compare/v2.15.2...v2.16.0) (2024-05-07)
### ⚠ BREAKING CHANGES
* **Input:** redesign `file` type without absolute positioning (#1712)
### Features
* **InputMenu/SelectMenu:** allow lazy search ([#1705](https://github.com/nuxt/ui/issues/1705)) ([7e6ba78](https://github.com/nuxt/ui/commit/7e6ba786816516ab5007a2ff15fc974cfdd796ab))
* **module:** HMR support with `@nuxtjs/tailwindcss` ([#1665](https://github.com/nuxt/ui/issues/1665)) ([821e15b](https://github.com/nuxt/ui/commit/821e15b696b03d0f5e20e001d39f86a8b3cec426))
* **Table:** allow providing a `<caption>` ([#1680](https://github.com/nuxt/ui/issues/1680)) ([3fca668](https://github.com/nuxt/ui/commit/3fca66857d3616bf24a1b0579c90179a7883869d))
* **useToast:** allow clearing all notifications ([#1695](https://github.com/nuxt/ui/issues/1695)) ([82d619b](https://github.com/nuxt/ui/commit/82d619b2a75b9d08f3f5b314d37c30d77d8341e9))
### Bug Fixes
* **Breadcrumb:** pass `click` event to `ULink` ([5481dab](https://github.com/nuxt/ui/commit/5481dab53dbe0b28188b4a16811f3e8816d93edf))
* **Input:** redesign `file` type without absolute positioning ([#1712](https://github.com/nuxt/ui/issues/1712)) ([ed5c74d](https://github.com/nuxt/ui/commit/ed5c74dc17df784485eabc39c83e62ada9210a49))
* **Notification:** update timer when timeout prop changes ([#1673](https://github.com/nuxt/ui/issues/1673)) ([cba9ad7](https://github.com/nuxt/ui/commit/cba9ad78db58cb9228bb9c96f0469d43bde2bf3e))
* **Slideover:** export and clean types ([#1692](https://github.com/nuxt/ui/issues/1692)) ([bd3fa86](https://github.com/nuxt/ui/commit/bd3fa8658f84fb7bd96d322968462c5eaa987b86))
* **Table:** provide `aria-sort` for sortable table headings ([#1675](https://github.com/nuxt/ui/issues/1675)) ([6f60fa9](https://github.com/nuxt/ui/commit/6f60fa9a980020f6a5afc2916e699a7f9a47e8ce))
## [2.15.2](https://github.com/nuxt/ui/compare/v2.15.1...v2.15.2) (2024-04-12)

View File

@@ -30,16 +30,6 @@ Read more on [ui.nuxt.com](https://ui.nuxt.com)
npx nuxi@latest module add ui
```
Then, register the module in your `nuxt.config.ts`:
```js
export default defineNuxtConfig({
modules: [
'@nuxt/ui'
]
})
```
If you want latest updates, please use `@nuxt/ui-edge` in your `package.json`:
```json

View File

@@ -30,7 +30,7 @@
<script setup lang="ts">
import { withoutTrailingSlash } from 'ufo'
import { debounce } from 'perfect-debounce'
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
import type { ParsedContent } from '@nuxt/content'
const searchRef = ref()

View File

@@ -0,0 +1,34 @@
<template>
<svg viewBox="0 0 1440 181" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1_1086_7239" fill="white">
<path d="M0 0H1440V181H0V0Z" />
</mask>
<path d="M0 0H1440V181H0V0Z" fill="url(#paint0_linear_1086_7239)" fill-opacity="0.22" />
<path d="M0 2H1440V-2H0V2Z" fill="url(#paint1_linear_1086_7239)" mask="url(#path-1-inside-1_1086_7239)" />
<defs>
<linearGradient
id="paint0_linear_1086_7239"
x1="720"
y1="0"
x2="720"
y2="181"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="rgb(var(--color-primary-DEFAULT))" />
<stop offset="1" stop-color="rgb(var(--color-primary-DEFAULT))" stop-opacity="0" />
</linearGradient>
<linearGradient
id="paint1_linear_1086_7239"
x1="0"
y1="90.5"
x2="1440"
y2="90.5"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="rgb(var(--color-primary-DEFAULT))" stop-opacity="0" />
<stop offset="0.395" stop-color="rgb(var(--color-primary-DEFAULT))" />
<stop offset="1" stop-color="rgb(var(--color-primary-DEFAULT))" stop-opacity="0" />
</linearGradient>
</defs>
</svg>
</template>

View File

@@ -46,7 +46,7 @@
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
import type { NavItem } from '@nuxt/content'
import type { HeaderLink } from '#ui-pro/types'
defineProps<{

View File

@@ -1,8 +1,4 @@
<script lang="ts" setup>
defineOptions({
inheritAttrs: false
})
defineProps({
title: {
type: String,

View File

@@ -1,15 +1,15 @@
<template>
<UAlert
title="Customize Alert Avatar"
description="Insert custom content into the avatar slot!"
:avatar="{
src: 'https://avatars.githubusercontent.com/u/739984?v=4',
alt: 'Avatar'
<UAlert
title="Customize Alert Avatar"
description="Insert custom content into the avatar slot!"
:avatar="{
src: 'https://avatars.githubusercontent.com/u/739984?v=4',
alt: 'Avatar'
}"
>
<template #avatar="{ avatar }">
<UAvatar
v-bind="avatar"
<UAvatar
v-bind="avatar"
chip-color="primary"
chip-text=""
chip-position="top-right"
@@ -17,4 +17,3 @@
</template>
</UAlert>
</template>

View File

@@ -7,4 +7,3 @@
</template>
</UAlert>
</template>

View File

@@ -7,7 +7,8 @@ const groups = [{
return []
}
const users = await $fetch<any[]>('https://jsonplaceholder.typicode.com/users', { params: { q } })
// @ts-ignore
const users: any[] = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email }))
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { NavItem, ParsedContent } from '@nuxt/content/dist/runtime/types'
import type { NavItem, ParsedContent } from '@nuxt/content'
import type { Button } from '#ui/types'
const commandPaletteRef = ref()

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import { useMouse, useWindowScroll } from '@vueuse/core'
const { x, y } = useMouse()
const { y: windowY } = useWindowScroll()

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import { useMouse, useWindowScroll } from '@vueuse/core'
const { x, y } = useMouse()
const { y: windowY } = useWindowScroll()

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import { useMouse, useWindowScroll } from '@vueuse/core'
const { x, y } = useMouse()
const { y: windowY } = useWindowScroll()

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import { useMouse, useWindowScroll } from '@vueuse/core'
const { x, y } = useMouse()
const { y: windowY } = useWindowScroll()

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import { string, objectAsync, email, minLength, type Input } from 'valibot'
import * as v from 'valibot'
import type { FormSubmitEvent } from '#ui/types'
const schema = objectAsync({
email: string([email('Invalid email')]),
password: string([minLength(8, 'Must be at least 8 characters')])
const schema = v.object({
email: v.pipe(v.string(), v.email('Invalid email')),
password: v.pipe(v.string(), v.minLength(8, 'Must be at least 8 characters'))
})
type Schema = Input<typeof schema>
type Schema = v.InferOutput<typeof schema>
const state = reactive({
email: '',
@@ -21,7 +21,7 @@ async function onSubmit (event: FormSubmitEvent<Schema>) {
</script>
<template>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UForm :schema="v.safeParser(schema)" :state="state" class="space-y-4" @submit="onSubmit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>

View File

@@ -5,7 +5,7 @@ const selected = ref()
async function search (q: string) {
loading.value = true
const users = await $fetch<any[]>('https://jsonplaceholder.typicode.com/users', { params: { q } })
const users: any[] = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
loading.value = false

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
const page = ref(1)
const items = ref(Array(50))
</script>
<template>
<UPagination
v-model="page"
:page-count="5"
:total="items.length"
:to="(page: number) => ({
query: { page },
// Hash is specified here to prevent the page from scrolling to the top
hash: '#links',
})"
/>
</template>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
const options = ref([
{ id: 1, name: 'bug' },
{ id: 2, name: 'documentation' },
{ id: 3, name: 'duplicate' },
{ id: 4, name: 'enhancement' },
{ id: 5, name: 'good first issue' },
{ id: 6, name: 'help wanted' },
{ id: 7, name: 'invalid' },
{ id: 8, name: 'question' },
{ id: 9, name: 'wontfix' }
])
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 = {
id: options.value.length + 1,
name: label.name
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
const showCreateOption = (query, results) => {
const lowercaseQuery = String.prototype.toLowerCase.apply(query || '')
return lowercaseQuery.length >= 3 && !results.find(option => {
return String.prototype.toLowerCase.apply(option['name'] || '') === lowercaseQuery
})
}
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
multiple
searchable
creatable
:show-create-option-when="showCreateOption"
placeholder="Select labels"
/>
</template>

View File

@@ -5,7 +5,7 @@ const selected = ref([])
async function search (q: string) {
loading.value = true
const users = await $fetch<any[]>('https://jsonplaceholder.typicode.com/users', { params: { q } })
const users: any[] = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
loading.value = false

View File

@@ -8,6 +8,16 @@ const isOpen = ref(false)
<USlideover v-model="isOpen">
<div class="p-4 flex-1">
<UButton
color="gray"
variant="ghost"
size="sm"
icon="i-heroicons-x-mark-20-solid"
class="flex sm:hidden absolute end-5 top-5 z-10"
square
padded
@click="isOpen = false"
/>
<Placeholder class="h-full" />
</div>
</USlideover>

View File

@@ -7,8 +7,22 @@ const isOpen = ref(false)
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen">
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<UCard
class="flex flex-col flex-1"
:ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
>
<template #header>
<UButton
color="gray"
variant="ghost"
size="sm"
icon="i-heroicons-x-mark-20-solid"
class="flex sm:hidden absolute end-5 top-5 z-10"
square
padded
@click="isOpen = false"
/>
<Placeholder class="h-8" />
</template>

View File

@@ -23,7 +23,7 @@ const emits = defineEmits<{
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="emits('close')" />
</div>
</template>
<Placeholder class="h-full" />
</UCard>
</USlideover>

View File

@@ -8,6 +8,17 @@ const isOpen = ref(false)
<USlideover v-model="isOpen" :overlay="false">
<div class="p-4 flex-1">
<UButton
color="gray"
variant="ghost"
size="sm"
icon="i-heroicons-x-mark-20-solid"
class="flex sm:hidden absolute end-5 top-5 z-10"
square
padded
@click="isOpen = false"
/>
<Placeholder class="h-full" />
</div>
</USlideover>

View File

@@ -8,6 +8,17 @@ const isOpen = ref(false)
<USlideover v-model="isOpen" :transition="false">
<div class="p-4 flex-1">
<UButton
color="gray"
variant="ghost"
size="sm"
icon="i-heroicons-x-mark-20-solid"
class="flex sm:hidden absolute end-5 top-5 z-10"
square
padded
@click="isOpen = false"
/>
<Placeholder class="h-full" />
</div>
</USlideover>

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
const columns = [{
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}, {
key: 'actions'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
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'
}]
</script>
<template>
<UTable :rows="people" :columns="columns">
<template #caption>
<caption>Employees of ACME</caption>
</template>
</UTable>
</template>

View File

@@ -0,0 +1,49 @@
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
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'
}]
</script>
<template>
<UTable :rows="people">
<template #expand="{ row }">
<div class="p-4">
<pre>{{ row }}</pre>
</div>
</template>
</UTable>
</template>

View File

@@ -1,13 +1,16 @@
<script setup lang="ts">
const items = [{
label: 'Tab1',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
icon: 'i-heroicons-arrow-down-tray',
disabled: true,
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]
</script>

View File

@@ -1,12 +1,15 @@
<script setup lang="ts">
const items = [{
label: 'Tab1',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
icon: 'i-heroicons-arrow-down-tray',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]

View File

@@ -17,13 +17,7 @@ const items = [{
<template>
<UTabs :items="items" class="w-full">
<template #default="{ item, index, selected }">
<div class="flex items-center gap-2 relative truncate">
<UIcon :name="item.icon" class="w-4 h-4 flex-shrink-0" />
<span class="truncate">{{ index + 1 }}. {{ item.label }}</span>
<span v-if="selected" class="absolute -right-4 w-2 h-2 rounded-full bg-primary-500 dark:bg-primary-400" />
</div>
<span class="truncate" :class="[selected && 'text-primary-500 dark:text-primary-400']">{{ index + 1 }}. {{ item.label }}</span>
</template>
</UTabs>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
const items = [{
label: 'Getting Started',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Installation',
icon: 'i-heroicons-arrow-down-tray',
content: 'And, this is the content for Tab2'
}, {
label: 'Theming',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]
</script>
<template>
<UTabs :items="items" class="w-full">
<template #icon="{ item, selected }">
<UIcon :name="item.icon" class="w-4 h-4 flex-shrink-0 mr-2" :class="[selected && 'text-primary-500 dark:text-primary-400']" />
</template>
</UTabs>
</template>

View File

@@ -1,12 +1,15 @@
<script setup lang="ts">
const items = [{
label: 'Tab1',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
icon: 'i-heroicons-arrow-down-tray',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]
</script>

View File

@@ -1,12 +1,15 @@
<script setup lang="ts">
const items = [{
label: 'Tab1',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
icon: 'i-heroicons-arrow-down-tray',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]

View File

@@ -1,12 +1,15 @@
<script setup lang="ts">
const items = [{
label: 'Tab1',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
icon: 'i-heroicons-arrow-down-tray',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]
</script>

View File

@@ -0,0 +1,135 @@
<script setup lang="ts">
import { breakpointsTailwind } from '@vueuse/core'
const props = withDefaults(defineProps<{
contributors: {
username: string
}[]
level?: number
max?: number
}>(), {
level: 0,
max: 4
})
const contributors = computed(() => props.contributors.slice(0, 5))
const el = ref(null)
const { width } = useElementSize(el)
const breakpoints = useBreakpoints(breakpointsTailwind)
const smallerThanSm = breakpoints.smaller('sm')
</script>
<template>
<div
class="isolate rounded-full relative circle w-full aspect-[1/1] p-8 sm:p-12 md:p-14 lg:p-10 xl:p-16 before:absolute before:inset-px before:bg-white dark:before:bg-gradient-to-b dark:before:from-gray-950 dark:before:to-gray-900 before:rounded-full z-[--level]"
:style="{
'--duration': `${((level + 1) * 5)}s`,
'--level': level + 1
}"
>
<HomeContributors
v-if="(level + 1) < max"
:max="max"
:level="level + 1"
:contributors="props.contributors.slice(5)"
/>
<div
ref="el"
class="avatars absolute inset-0 grid place-items-center"
:style="{
'--total': contributors.length,
'--offset': `${width / 2}px`
}"
>
<UTooltip
v-for="(contributor, index) in contributors"
:key="contributor.username"
:text="contributor.username"
:style="{
'--index': index + 1
}"
class="avatar flex absolute"
>
<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="ring-2 ring-gray-200 dark:ring-gray-700 lg:hover:ring-gray-900 dark:lg:hover:ring-white transition"
width="40"
height="40"
:size="smallerThanSm ? 'xs' : 'sm'"
loading="lazy"
>
<NuxtLink :to="`https://github.com/${contributor.username}`" :aria-label="contributor.username" target="_blank" class="focus:outline-none" tabindex="-1">
<span class="absolute inset-0" aria-hidden="true" />
</NuxtLink>
</UAvatar>
</UTooltip>
</div>
</div>
</template>
<style scoped>
.circle:after {
--start: 0deg;
--end: 360deg;
--border-color: rgb(var(--color-gray-200));
--highlight-color: rgb(var(--color-gray-700));
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: -1px;
opacity: 1;
border-radius: 9999px;
z-index: -1;
background: var(--border-color);
@supports (background: paint(houdini)) {
background: linear-gradient(var(--angle), var(--border-color), var(--border-color), var(--border-color), var(--border-color), var(--highlight-color));
animation: var(--duration) rotate linear infinite;
}
}
.dark .circle:after {
--border-color: rgb(var(--color-gray-700));
--highlight-color: white;
}
.avatars {
--start: calc(var(--level) * 36deg);
--end: calc(360deg + (var(--level) * 36deg));
transform: rotate(var(--angle));
animation: calc(var(--duration) + 60s) rotate linear infinite;
}
.avatar {
--deg: calc(var(--index) * (360deg / var(--total)));
--transformX: calc(cos(var(--deg)) * var(--offset));
--transformY: calc(sin(var(--deg)) * var(--offset));
transform: translate(var(--transformX), var(--transformY)) rotate(calc(360deg - var(--angle)));
}
@keyframes rotate {
from {
--angle: var(--start);
}
to {
--angle: var(--end);
}
}
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: true;
}
</style>

View File

@@ -31,7 +31,7 @@ onMounted(() => {
</script>
<template>
<ULandingGrid ref="section" class="lg:grid-cols-10 lg:gap-8">
<ULandingGrid ref="section" class="lg:grid-cols-10 lg:gap-8 overflow-hidden p-px">
<div :ref="(el) => (refs[1] = el)" class="col-span-8 flex items-center animate-top">
<RangeExample />
</div>

View File

@@ -32,18 +32,24 @@ The Nuxt Starter template is available from the `nuxi init` command.
npx nuxi@latest init -t ui
```
Please check [nuxt/starter](https://github.com/nuxt/starter/tree/ui) for details.
## Modules
Nuxt UI will automatically install the [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/), [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) and [nuxt-icon](https://github.com/nuxt-modules/icon) modules for you.
Nuxt UI will automatically install the [@nuxt/icon](https://github.com/nuxt/icon), [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/) and [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) modules for you.
::callout{icon="i-heroicons-exclamation-triangle"}
You should remove them from your `modules` and `dependencies` if you've previously installed them.
::
### `@nuxt/icon`
This module is installed to provide an easy way to use icons in your application. You can use the `UIcon` component to display any icon from Iconify.
::callout{icon="i-heroicons-light-bulb"}
You can read more about this in the [Theming](/getting-started/theming#icons) section.
::
### `@nuxtjs/tailwindcss`
This module is pre-configured and will automatically load the following plugins:
@@ -86,14 +92,6 @@ This module is installed to provide dark mode support out of the box thanks to t
You can read more about this in the [Theming](/getting-started/theming#dark-mode) section.
::
### `nuxt-icon`
This module is installed when using the `dynamic` prop on the `Icon` component or globally through the `ui.icons.dynamic` option in your `app.config.ts`.
::callout{icon="i-heroicons-light-bulb"}
You can read more about this in the [Theming](/getting-started/theming#dynamic-icons) section and on the [Icon](/components/icon) component page.
::
## TypeScript
This module is written in TypeScript and provides typings for all the components and composables. You can look at the [source code](https://github.com/nuxt/ui/tree/dev/src/runtime/types) to see all the available types.
@@ -231,7 +229,6 @@ You can also add the following to your `.vscode/settings.json` to enable Intelli
|-----------------------|-----------------|-------------------------------------------------------------------------------------------------------------|
| `prefix` | `u` | Define the prefix of the imported components. |
| `global` | `false` | Expose components globally. |
| `icons` | `['heroicons']` | Icon collections to load. |
| `safelistColors` | `['primary']` | Force safelisting of colors to need be purged. |
| `disableGlobalStyles` | `false` | Disable [global CSS styles](https://github.com/nuxt/ui/blob/dev/src/runtime/ui.css) injected by the module. |
@@ -241,8 +238,7 @@ Configure options in your `nuxt.config.ts` as such:
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
ui: {
global: true,
icons: ['mdi', 'simple-icons']
global: true
}
})
```

View File

@@ -106,7 +106,7 @@ This can also happen when you bind a dynamic color to a component: `<UBadge :col
### `app.config.ts`
You can find the config of each component in the [`ui.config/`](https://github.com/nuxt/ui/tree/dev/src/runtime/ui.config) directory. You can override those classes in your own `app.config.ts`.
You can override component config in your own `app.config.ts`:
```ts [app.config.ts]
export default defineAppConfig({
@@ -118,6 +118,8 @@ export default defineAppConfig({
})
```
The available options for each component should auto-complete, and you can review the defaults for each component using your IDE's function such as `Cmd`+`Click` (these files can be found in [`src/runtime/ui.config/`](https://github.com/nuxt/ui/tree/dev/src/runtime/ui.config)).
Thanks to [tailwind-merge](https://github.com/dcastil/tailwind-merge), the `app.config.ts` is smartly merged with the default config. This means you don't have to rewrite everything.
You can change this behavior by setting `strategy` to `override` in your `app.config.ts`:
@@ -258,6 +260,7 @@ const isDark = computed({
}
})
</script>
<template>
<ClientOnly>
<UButton
@@ -277,9 +280,11 @@ const isDark = computed({
## Icons
You can use any icon (100,000+) from [Iconify](https://iconify.design/).
Thanks to [`@nuxt/icon`](https://github.com/nuxt/icon), add 200,000+ ready to use icons to your Nuxt application based on [Iconify](https://iconify.design).
Some components have an `icon` prop that allows you to add an icon to the component.
You can use any name from the https://icones.js.org collection such as the `i-` prefix (for example, `i-heroicons-cog`) with:
- any `icon` prop available across the components:
```vue
<template>
@@ -287,31 +292,17 @@ Some components have an `icon` prop that allows you to add an icon to the compon
</template>
```
You can also use the [Icon](/components/icon) component to add an icon anywhere in your app by following this pattern: `i-{collection_name}-{icon_name}`.
- the `UIcon` component to use icons anywhere:
```vue
<template>
<UIcon name="i-heroicons-moon" />
<UIcon name="i-heroicons-moon" class="w-5 h-5 text-primary-500" />
</template>
```
### Collections
By default, the module uses [Heroicons](https://heroicons.com/) but you can change it from the module options in your `nuxt.config.ts`.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
ui: {
icons: ['mdi', 'simple-icons']
}
})
```
::callout{icon="i-heroicons-light-bulb"}
Search the icon you want to use on https://icones.js.org built by [@antfu](https://github.com/antfu).
::
Thanks to [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons) plugin, only the icons you use in your app will be bundled in your CSS. However, you need to install the icon collections you specified in the `ui.icons` key:
It's highly recommended to install the icons collections locally with:
::code-group
@@ -329,55 +320,11 @@ npm install @iconify-json/{collection_name}
::
If you choose to use the full `@iconify/json` icon collection (50MB), you can specifiy `icons: 'all'` or `icons: {}` in your `nuxt.config.ts` to use any icon in your app.
For example, to use the `i-uil-github` icon, install it's collection with `@iconify-json/uil`. This way the icons can be served locally or from your serverless functions, which is faster and more reliable on both SSR and client-side.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
ui: {
icons: {}
}
})
```
### Custom config
If you have specific needs, like using a custom icon collection, you can use the `icons` option in your `nuxt.config.ts` as an object to override the config of the [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
```ts [nuxt.config.ts]
import { getIconCollections } from '@egoist/tailwindcss-icons'
export default defineNuxtConfig({
ui: {
icons: {
// might solve stretch bug on generate, see https://github.com/egoist/tailwindcss-icons/issues/23
extraProperties: {
'mask-size': 'contain',
'mask-position': 'center'
},
collections: {
foo: {
icons: {
'arrow-left': {
// svg body
body: '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />',
// svg width and height, optional
width: 24,
height: 24
}
}
},
...getIconCollections(['heroicons', 'simple-icons'])
}
}
}
})
```
### Dynamic icons
The `Icon` component also has a `dynamic` prop to use the [nuxt-icon](https://github.com/nuxt-modules/icon/) module instead of the [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
Read more about this in the [Icon](/components/icon#dynamic) component page.
::callout{icon="i-heroicons-light-bulb" to="https://github.com/nuxt/icon?tab=readme-ov-file#custom-local-collections" target="_blank"}
Read more about custom collections in the `@nuxt/icon` documentation.
::
### Defaults

View File

@@ -3,7 +3,7 @@ description: Display togglable accordion panels.
links:
- label: Disclosure
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/disclosure'
to: 'https://headlessui.com/v1/vue/disclosure'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Accordion.vue

View File

@@ -163,18 +163,22 @@ This can be handy when you want to display HTML content. To achieve this, you ca
:component-example{component="alert-example-html"}
### `icon` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
### `icon`
Use the `#icon` slot to customize the displayed icon.
:component-example{component="alert-example-icon"}
### `avatar` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
### `avatar`
Use the `#avatar` slot to customize the displayable avatar.
:component-example{component="alert-example-avatar"}
### `actions`
Use the `#actions` slot to add custom user interaction elements.
## Props
:component-props

View File

@@ -98,7 +98,7 @@ The number of indicators will be automatically generated based on the number of
:component-example{component="carousel-example-indicators-size"}
## Autoplay :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
## Autoplay
You can easily implement an autoplay behavior using the exposed [API](#api) through a template ref.

View File

@@ -87,6 +87,10 @@ slots:
[Label]{.italic}
::
### `help` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Like the `#label` slot, use the `#help` slot to override the content of the help text.
## Props
:component-props

View File

@@ -4,7 +4,7 @@ description: Add a customizable command palette to your app.
links:
- label: 'Combobox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/combobox'
to: 'https://headlessui.com/v1/vue/combobox'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/navigation/CommandPalette.vue

View File

@@ -66,7 +66,7 @@ excludedProps:
---
::
### Size :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
### Size
Use the `size` prop to change the size of the divider.

View File

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

View File

@@ -196,7 +196,7 @@ Use the `#description` slot to set the custom content for description.
::component-card
---
slots:
description: Write only valid email address <UIcon name="i-heroicons-arrow-right-20-solid" />
description: Write only valid email address <UIcon name="i-heroicons-information-circle" />
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
props:
label: 'Email'

View File

@@ -8,13 +8,13 @@ links:
## 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), [Valibot](https://valibot.dev/), or your own validation logic.
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://github.com/fabian-hiller/valibot), or your own validation logic.
It works with the [FormGroup](/components/input) component to display error messages around form elements automatically.
It works with the [FormGroup](/components/form-group) component to display error messages around form elements automatically.
The form component requires two props:
- `state` - a reactive object holding the form's state.
- `schema` - a schema object from [Yup](#yup), [Zod](#zod), [Joi](#joi), or [Valibot](#valibot).
- `schema` - a schema object from a validation library like [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi) or [Valibot](https://github.com/fabian-hiller/valibot).
::callout{icon="i-heroicons-light-bulb"}
Note that **no validation library is included** by default, so ensure you **install the one you need**.
@@ -63,7 +63,7 @@ The validation function must return a list of errors with the following attribut
- `path` - Path to the form element corresponding to the `name` attribute.
::callout{icon="i-heroicons-light-bulb"}
Note: this can be used alongside the `schema` prop to handle complex use cases.
Note that it can be used alongside the `schema` prop to handle complex use cases.
::
::component-example
@@ -105,7 +105,7 @@ defineExpose({
</script>
<template>
<UForm ref="form" :model="model" :validate="validateWithVuelidate">
<UForm ref="form" :state="model" :validate="validateWithVuelidate">
<slot />
</UForm>
</template>
@@ -184,13 +184,13 @@ Take a look at the component!
## Error event
You can listen to the `@error` event to handle errors. This event is triggered when the form is validated and contains an array of `FormError` objects with the following fields:
You can listen to the `@error` event to handle errors. This event is triggered when the form is submitted and contains an array of `FormError` objects with the following fields:
- `id` - the identifier of the form element.
- `path` - the path to the form element matching the `name`.
- `message` - the error message to display.
Here is an example of how to focus the first form element with an error:
Here's an example that focuses the first input element with an error after the form is submitted:
::component-example
---

View File

@@ -1,56 +1,27 @@
---
description: Display any icon (100,000+) from Iconify.
description: Add 200,000+ ready to use icons to your Nuxt application, based on Iconify.
links:
- label: GitHub
- label: Iconify
icon: i-simple-icons-iconify
to: https://icones.js.org
- label: Nuxt Icon
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Icon.vue
to: https://github.com/nuxt/icon
---
## Usage
Use the `name` prop by following this pattern: `i-{collection_name}-{icon_name}`. You can search any icon from [Iconify](https://iconify.design/) using https://icones.js.org.
You can use any name from the https://icones.js.org collection such as the `i-` prefix:
::component-card
---
baseProps:
class: 'w-5 h-5'
props:
name: 'i-heroicons-light-bulb'
---
::
::callout{icon="i-heroicons-exclamation-triangle"}
You won't be able to use all icons in the `name` prop here as icons are bundled using [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons).
::
::callout{icon="i-heroicons-light-bulb"}
Don't forget to install and specify the icon collections you need in your `nuxt.config.ts`, read more about this in [Theming](/getting-started/theming#icons).
It's highly recommended to install the icons collections you need, read more about this in [Theming](/getting-started/theming#icons).
::
### Dynamic
You can use the `dynamic` prop to enable dynamic icon loading. This will use [`nuxt-icon`](https://github.com/nuxt-modules/icon) instead and allow you to use any icon from [Iconify](https://iconify.design/) as well as the `{collection_name}:{icon_name}` pattern.
This can be quite useful when using [dynamic class names](https://tailwindcss.com/docs/content-configuration#dynamic-class-names) or for icons that are not bundled by default (fetched from a database for example).
::component-card
---
props:
name: 'i-ph-rocket-launch'
dynamic: true
---
::
You can also change the default behavior of the `dynamic` prop by setting the `ui.icons.dynamic` option in your `app.config.ts`.
```ts [app.config.ts]
export default defineAppConfig({
ui: {
icons: {
dynamic: true
}
}
})
```
## Props
:component-props

View File

@@ -4,7 +4,7 @@ description: Display an autocomplete input with real-time suggestions.
links:
- label: 'Combobox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/combobox'
to: 'https://headlessui.com/v1/vue/combobox'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/InputMenu.vue
@@ -130,6 +130,8 @@ Pass a function to the `search` prop to customize the search behavior and filter
Use the `debounce` prop to adjust the delay of the function.
Use the `searchLazy` prop to control the immediacy of data requests.
::component-example
---
component: 'input-menu-example-search-async'

View File

@@ -71,10 +71,12 @@ props:
Use the `type` prop to change the input type, the default `type` is set to `text`, you can check all the available types at [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types).
Some types have been implemented in their own components, such as [Checkbox](/components/checkbox), [Radio](/components/radio-group), etc. and others have been styled like `file` for example. :u-badge{label="New" class="!rounded-full" variant="subtle"}
Some types have been implemented in their own components, such as [Checkbox](/components/checkbox), [Radio](/components/radio-group), etc. and others have been styled like `file` for example.
::component-card
---
baseProps:
icon: 'i-heroicons-folder'
props:
type: 'file'
size: sm

View File

@@ -3,7 +3,7 @@ description: Display a modal within your application.
links:
- label: 'Dialog'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/dialog'
to: 'https://headlessui.com/v1/vue/dialog'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Modal.vue
@@ -59,7 +59,7 @@ Set the `fullscreen` prop to `true` to enable it.
:component-example{component="modal-example-fullscreen"}
### Control programmatically :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
### Control programmatically
First of all, add the `Modals` component to your app, preferably inside `app.vue`.
@@ -85,7 +85,7 @@ Then, you can use the `useModal` composable to control your modals within your a
:component-example{component="modal-example-component" hiddenPreview label="modal.vue" }
::
Additionally, you can close the modal within the modal component by calling `modal.close()`.
Additionally, you can close the modal within the modal component by calling `modal.close()`.
## Props

View File

@@ -29,7 +29,7 @@ export default defineAppConfig({
ui: {
notifications: {
// Show toasts at the top right of the screen
position: 'top-0 bottom-auto'
position: 'top-0 right-0'
}
}
})

View File

@@ -46,6 +46,12 @@ props:
---
::
### Links
Use the `to` property to transform buttons into links. Note that it must be a function that receives the page number and returns a route destination.
:component-example{component="pagination-example-to"}
### Disabled
Use the `disabled` prop to disable all the buttons.

View File

@@ -3,7 +3,7 @@ description: Display a non-modal dialog that floats around a trigger element.
links:
- label: 'Popover'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/popover'
to: 'https://headlessui.com/v1/vue/popover'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Popover.vue

View File

@@ -126,6 +126,10 @@ slots:
[Label]{.italic}
::
### `help` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Like the `#label` slot, use the `#help` slot to override the content of the help text.
### `legend`
Use the `#legend` slot to override the content of the legend.

View File

@@ -4,7 +4,7 @@ description: Display a select menu with advanced features.
links:
- label: 'Listbox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/listbox'
to: 'https://headlessui.com/v1/vue/listbox'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/SelectMenu.vue
@@ -87,7 +87,7 @@ Use the `searchable` prop to enable search.
Use the `searchable-placeholder` prop to set a different placeholder.
This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox).
This will use Headless UI [Combobox](https://headlessui.com/v1/vue/combobox) component instead of [Listbox](https://headlessui.com/v1/vue/listbox).
::component-card
---
@@ -150,6 +150,8 @@ Pass a function to the `searchable` prop to customize the search behavior and fi
Use the `debounce` prop to adjust the delay of the function.
Use the `searchableLazy` prop to control the immediacy of data requests.
::component-example
---
component: 'select-menu-example-search-async'
@@ -186,6 +188,18 @@ componentProps:
---
::
Pass a function to the `show-create-option-when` prop to control wether or not to show the create option. This function takes two arguments: the query (as the first argument) and an array of current results (as the second argument). It should return true to display the create option. :u-badge{label="New" class="!rounded-full" variant="subtle"}
The example below shows how to make the create option visible when the query is at least three characters long and does not exactly match any of the current results (case insensitive).
::component-example
---
component: 'select-menu-example-creatable-function'
componentProps:
class: 'w-full lg:w-48'
---
::
## Popper
Use the `popper` prop to customize the popper instance.

View File

@@ -3,7 +3,7 @@ description: Display a dialog that slides in from the edge of the screen.
links:
- label: 'Dialog'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/dialog'
to: 'https://headlessui.com/v1/vue/dialog'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Slideover.vue
@@ -53,7 +53,7 @@ defineShortcuts({
</script>
```
### Control programmatically :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
### Control programmatically
First of all, add the `USlideovers` component to your app, preferably inside `app.vue`.

View File

@@ -29,6 +29,7 @@ Use the `columns` prop to configure which columns to display. It's an array of o
- `sortable` - Whether the column is sortable. Defaults to `false`.
- `direction` - The sort direction to use on first click. Defaults to `asc`.
- `class` - The class to apply to the column cells.
- `rowClass` - The class to apply to the data column cells. :u-badge{label="New" class="!rounded-full" variant="subtle"}
- `sort` - Pass your own `sort` function. Defaults to a simple _greater than_ / _less than_ comparison.
::component-example{class="grid"}
@@ -49,7 +50,7 @@ extraClass: 'overflow-hidden'
padding: false
component: 'table-example-columns-selectable'
componentProps:
class: 'flex-1'
class: 'flex-1 flex-col overflow-hidden'
---
::
@@ -255,7 +256,7 @@ componentProps:
::
::callout{icon="i-heroicons-light-bulb"}
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 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/v1/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.
@@ -282,7 +283,7 @@ extraClass: 'overflow-hidden'
padding: false
component: 'table-example-searchable'
componentProps:
class: 'flex-1'
class: 'flex-1 flex-col overflow-hidden'
---
::
@@ -295,6 +296,19 @@ You can easily use the [Pagination](/components/pagination) component to paginat
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-paginable'
componentProps:
class: 'flex-1 flex-col overflow-hidden'
---
::
### Expandable :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
You can use the `expand` slot to display extra information about a row. You will have access to the `row` property in the slot scope.
::component-example{class="grid"}
---
padding: false
component: 'table-example-expandable'
componentProps:
class: 'flex-1'
---
@@ -450,6 +464,19 @@ componentProps:
---
::
### `caption`
Use the `#caption` slot to customize the table's caption.
::component-example
---
padding: false
component: 'table-example-caption-slot'
componentProps:
class: 'flex-1'
---
::
## Props
:component-props

View File

@@ -11,6 +11,7 @@ links:
Pass an array to the `items` prop of the Tabs component. Each item can have the following properties:
- `label` - The label of the item.
- `icon` - The icon of the item.
- `slot` - A key to customize the item with a slot.
- `content` - The content to display in the panel by default.
- `disabled` - Determines whether the item is disabled or not.
@@ -63,6 +64,8 @@ componentProps:
---
::
You can use the `content` prop and set it to `false` to avoid the rendering of the HTML content if you don't need it.
### Control the selected index
Use a `v-model` to control the selected index.
@@ -89,6 +92,12 @@ Use the `#default` slot to customize the content of the trigger buttons. You wil
:component-example{component="tabs-example-default-slot"}
### `icon` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Use the `#icon` slot to customize the icon of the trigger buttons. You will have access to the `item`, `index`, `selected` and `disabled` in the slot scope.
:component-example{component="tabs-example-icon-slot"}
### `item`
Use the `#item` slot to customize the items content. You will have access to the `item`, `index` and `selected` properties in the slot scope.

View File

@@ -119,8 +119,7 @@ props:
---
::
Use the `maxrows` prop to set a maximum number of rows when autoresizing. If set to `0`, the Textarea will infinitely grow up. :u-badge{label="New" class="!rounded-full" variant="subtle"}
Use the `maxrows` prop to set a maximum number of rows when autoresizing. If set to `0`, the Textarea will infinitely grow up.
::component-card
---
baseProps:

View File

@@ -3,7 +3,7 @@ description: Display a toggle field.
links:
- label: 'Switch'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/switch'
to: 'https://headlessui.com/v1/vue/switch'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Toggle.vue
@@ -52,7 +52,7 @@ excludedProps:
---
::
### Loading :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
### Loading
Use the `loading` prop to show a loading icon and disable the Toggle.

View File

@@ -67,14 +67,19 @@ sections:
color: black
size: lg
icon: i-heroicons-cube
- label: Star on GitHub
to: https://github.com/nuxt/ui
target: _blank
color: white
size: lg
icon: i-simple-icons-github
cta:
title: Trusted and supported by our<br class="hidden lg:block"> amazing community
title: Built and driven by an <span class="text-primary">amazing community</span>
align: center
users:
- name: Benjamin Canac
username: benjamincanac
to: https://twitter.com/benjamincanac
- name: Romain Hamel
username: romhml
to: https://github.com/romhml
- name: Neil Richter
username: noook
to: https://nook.sh/
pro:
title: Upgrade to <span class="text-primary">Nuxt UI Pro</span>
description: 'Nuxt UI Pro is a collection of premium Vue components built on top of Nuxt UI to create beautiful & responsive Nuxt applications in minutes.<br>It includes all primitives to build landing pages, documentations, blogs, dashboards or entire SaaS products.'

View File

@@ -30,7 +30,7 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
import type { ParsedContent } from '@nuxt/content'
useSeoMeta({
title: 'Page not found',

View File

@@ -17,7 +17,7 @@
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
import type { NavItem } from '@nuxt/content'
const nav = inject<Ref<NavItem[]>>('navigation')

View File

@@ -1,7 +1,7 @@
import { createResolver } from '@nuxt/kit'
import colors from 'tailwindcss/colors'
import module from '../src/module'
import { excludeColors } from '../src/colors'
import { excludeColors } from '../src/runtime/utils/colors'
import pkg from '../package.json'
const { resolve } = createResolver(import.meta.url)
@@ -15,6 +15,7 @@ export default defineNuxtConfig({
'@nuxt/ui-pro',
process.env.NUXT_GITHUB_TOKEN && ['github:nuxt/ui-pro/.docs#dev', { giget: { auth: process.env.NUXT_GITHUB_TOKEN } }]
].filter(Boolean),
modules: [
'@nuxt/content',
'@nuxt/fonts',
@@ -27,16 +28,18 @@ export default defineNuxtConfig({
'nuxt-cloudflare-analytics',
'modules/content-examples-code'
],
runtimeConfig: {
public: {
version: pkg.version
}
},
ui: {
global: true,
icons: ['heroicons', 'simple-icons'],
safelistColors: excludeColors(colors)
},
content: {
highlight: {
langs: [
@@ -71,9 +74,11 @@ export default defineNuxtConfig({
} : undefined
}
},
image: {
provider: 'ipx'
},
nitro: {
prerender: {
routes: [
@@ -87,10 +92,12 @@ export default defineNuxtConfig({
ignore: !process.env.NUXT_UI_PRO_PATH && !process.env.NUXT_GITHUB_TOKEN ? ['/pro'] : []
}
},
routeRules: {
'/components': { redirect: '/components/accordion', prerender: false },
'/dev/components': { redirect: '/dev/components/accordion', prerender: false }
},
componentMeta: {
exclude: [
'@nuxt/content',
@@ -111,10 +118,12 @@ export default defineNuxtConfig({
exposed: false
}
},
cloudflareAnalytics: {
token: '1e2b0c5e9a214f0390b9b94e043d8d4c',
scriptPath: false
},
hooks: {
// Related to https://github.com/nuxt/nuxt/pull/22558
'components:extend': (components) => {
@@ -127,12 +136,20 @@ export default defineNuxtConfig({
})
}
},
typescript: {
strict: false
},
site: {
url: 'https://ui.nuxt.com'
},
vite: {
optimizeDeps: {
include: ['date-fns']
}
}
})
},
compatibilityDate: '2024-07-23'
})

View File

@@ -3,32 +3,30 @@
"private": true,
"type": "module",
"dependencies": {
"@nuxt/ui": "workspace:latest"
},
"devDependencies": {
"@iconify-json/heroicons": "^1.1.20",
"@iconify-json/simple-icons": "^1.1.99",
"@nuxt/content": "^2.12.1",
"@nuxt/eslint-config": "^0.3.6",
"@nuxt/fonts": "^0.6.1",
"@nuxt/image": "^1.5.0",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@1.1.0-28546155.4b9828b",
"@iconify-json/heroicons": "^1.1.22",
"@iconify-json/simple-icons": "^1.1.109",
"@iconify-json/vscode-icons": "^1.1.36",
"@nuxt/content": "^2.13.2",
"@nuxt/eslint-config": "^0.3.13",
"@nuxt/fonts": "^0.7.1",
"@nuxt/image": "^1.7.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@1.3.1-28695485.b267028",
"@nuxtjs/plausible": "^1.0.0",
"@octokit/rest": "^20.1.0",
"@vueuse/nuxt": "^10.9.0",
"@octokit/rest": "^21.0.1",
"@vueuse/nuxt": "^10.11.0",
"date-fns": "^3.6.0",
"eslint": "^8.57.0",
"joi": "^17.12.3",
"nuxt": "^3.11.2",
"joi": "^17.13.3",
"nuxt": "^3.12.4",
"nuxt-cloudflare-analytics": "^1.0.8",
"nuxt-component-meta": "^0.6.3",
"nuxt-og-image": "^2.2.4",
"prettier": "^3.2.5",
"typescript": "^5.4.5",
"ufo": "^1.5.3",
"nuxt-component-meta": "^0.6.4",
"nuxt-og-image": "^3.0.0-rc.64",
"prettier": "^3.3.3",
"ufo": "^1.5.4",
"v-calendar": "^3.1.2",
"valibot": "^0.30.0",
"valibot": "^0.36.0",
"yup": "^1.4.0",
"zod": "^3.22.4"
"zod": "^3.23.8"
}
}

View File

@@ -75,10 +75,7 @@ useSeoMeta({
ogDescription: page.value.description
})
defineOgImage({
component: 'Docs',
title: page.value.title,
description: page.value.description,
defineOgImageComponent('Docs', {
headline: headline.value
})

View File

@@ -102,77 +102,79 @@
<ULandingSection class="!pt-0 dark:bg-gradient-to-b from-gray-950/50 to-gray-900">
<ULandingCTA
align="left"
card
:card="false"
: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'
padding: '!p-0'
},
title: 'text-center lg:text-left',
links: 'justify-center lg:justify-start'
title: 'text-center lg:text-left lg:text-5xl',
description: 'mt-10 flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-8 lg:gap-16',
links: '-ml-3 justify-center lg:justify-start flex-wrap gap-y-3'
}"
>
<template #title>
<span v-html="page.cta.title" />
</template>
<template #links>
<ClientOnly>
<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"
width="40"
height="40"
size="md"
loading="lazy"
>
<NuxtLink :to="`https://github.com/${contributor.username}`" :aria-label="contributor.username" target="_blank" class="focus:outline-none" tabindex="-1">
<span class="absolute inset-0" aria-hidden="true" />
</NuxtLink>
</UAvatar>
</UTooltip>
</UAvatarGroup>
</ClientOnly>
</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">
<template #description>
<NuxtLink class="text-center lg:text-left group" to="https://npmjs.org/package/@nuxt/ui" target="_blank">
<p class="text-5xl 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">
<NuxtLink class="text-center lg:text-left group" to="https://github.com/nuxt/ui" target="_blank">
<p class="text-5xl 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>
<p>GitHub stars</p>
</NuxtLink>
</template>
<template #links>
<UButton
v-for="user in page.cta.users"
:key="user.username"
:to="user.to"
size="md"
color="gray"
variant="ghost"
target="_blank"
>
<UAvatar
:alt="user.username"
:src="`https://ipx.nuxt.com/s_80x80/gh_avatar/${user.username}`"
:srcset="`https://ipx.nuxt.com/s_160x160/gh_avatar/${user.username} 2x`"
width="80"
height="80"
size="md"
loading="lazy"
/>
<div class="text-left">
<p class="font-medium">
{{ user.name }}
</p>
<p class="text-gray-500 dark:text-gray-400 leading-4">
{{ `@${user.username}` }}
</p>
</div>
</UButton>
</template>
<div class="p-5 overflow-hidden flex">
<HomeContributors :contributors="module.contributors" />
</div>
</ULandingCTA>
</ULandingSection>
<template v-if="navigation.find(item => item._path === '/pro')">
<div class="relative">
<UDivider class="absolute inset-x-0" />
<div class="w-full relative overflow-hidden h-px bg-gradient-to-r from-gray-800 via-primary-400 to-gray-800 max-w-5xl mx-auto" />
</div>
<ULandingHero id="pro" :links="page.pro.links" :ui="{ title: 'sm:text-6xl' }" class="bg-gradient-to-b from-gray-50 dark:from-gray-950/50 to-white dark:to-gray-900 relative">
<template #top>
<Gradient class="absolute inset-x-0 top-0 w-full block" />
</template>
<template #title>
<span v-html="page.pro.title" />
</template>
@@ -407,7 +409,7 @@
</template>
<script setup lang="ts">
import type { ParsedContent, NavItem } from '@nuxt/content/dist/runtime/types'
import type { ParsedContent, NavItem } from '@nuxt/content'
import { useElementBounding, useWindowScroll, useElementSize, breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import type { HomeProBlock } from '~/types'
@@ -438,7 +440,7 @@ useSeoMeta({
const source = ref('npx nuxi@latest module add ui')
const sectionRef = ref()
const demoRef = ref()
const demoRef = ref(null)
const start = ref(0)
const { height } = useElementSize(demoRef)
@@ -448,9 +450,7 @@ const config = useRuntimeConfig().public
const { copy, copied } = useClipboard({ source })
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greaterOrEqual('md')
const lgAndLarger = breakpoints.greaterOrEqual('lg')
const xlAndLarger = breakpoints.greaterOrEqual('xl')
const { format } = Intl.NumberFormat('en', { notation: 'compact' })

View File

@@ -9,11 +9,7 @@ useSeoMeta({
description
})
defineOgImage({
component: 'Docs',
title,
description
})
defineOgImageComponent('Docs')
</script>
<template>

View File

@@ -69,11 +69,7 @@ useSeoMeta({
ogDescription: description
})
defineOgImage({
component: 'Docs',
title,
description
})
defineOgImageComponent('Docs')
</script>
<style scoped>

View File

@@ -9,11 +9,7 @@ useSeoMeta({
description
})
defineOgImage({
component: 'Docs',
title,
description
})
defineOgImageComponent('Docs')
const appConfig = useAppConfig()
const colorMode = useColorMode()

View File

@@ -1,6 +1,7 @@
{
"name": "@nuxt/ui",
"version": "2.15.2",
"version": "2.18.0",
"packageManager": "pnpm@9.5.0",
"repository": "nuxt/ui",
"homepage": "https://ui.nuxt.com",
"type": "module",
@@ -33,55 +34,53 @@
"test": "vitest"
},
"dependencies": {
"@egoist/tailwindcss-icons": "^1.7.4",
"@headlessui/tailwindcss": "^0.2.0",
"@headlessui/vue": "^1.7.19",
"@iconify-json/heroicons": "^1.1.20",
"@nuxt/kit": "^3.11.2",
"@nuxtjs/color-mode": "^3.4.0",
"@nuxtjs/tailwindcss": "^6.11.4",
"@headlessui/tailwindcss": "^0.2.1",
"@headlessui/vue": "^1.7.22",
"@iconify-json/heroicons": "^1.1.22",
"@nuxt/icon": "^1.2.1",
"@nuxt/kit": "^3.12.4",
"@nuxtjs/color-mode": "^3.4.2",
"@nuxtjs/tailwindcss": "^6.12.1",
"@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.12",
"@vueuse/core": "^10.9.0",
"@vueuse/integrations": "^10.9.0",
"@vueuse/math": "^10.9.0",
"@tailwindcss/typography": "^0.5.13",
"@vueuse/core": "^10.11.0",
"@vueuse/integrations": "^10.11.0",
"@vueuse/math": "^10.11.0",
"defu": "^6.1.4",
"fuse.js": "^6.6.2",
"nuxt-icon": "^0.6.10",
"ohash": "^1.1.3",
"pathe": "^1.1.2",
"scule": "^1.3.0",
"tailwind-merge": "^2.2.2",
"tailwindcss": "^3.4.3"
"tailwind-merge": "^2.4.0",
"tailwindcss": "^3.4.6"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.3.6",
"@nuxt/module-builder": "^0.5.5",
"@nuxt/test-utils": "^3.12.0",
"@nuxt/eslint-config": "^0.3.13",
"@nuxt/module-builder": "^0.8.1",
"@nuxt/test-utils": "^3.13.1",
"@release-it/conventional-changelog": "^8.0.1",
"@vue/test-utils": "^2.4.5",
"@vue/test-utils": "^2.4.6",
"eslint": "^8.57.0",
"happy-dom": "^14.7.1",
"joi": "^17.12.3",
"nuxt": "^3.11.2",
"release-it": "^17.2.0",
"typescript": "^5.4.5",
"happy-dom": "^14.12.3",
"joi": "^17.13.3",
"nuxt": "^3.12.4",
"release-it": "^17.6.0",
"unbuild": "^2.0.0",
"valibot": "^0.30.0",
"vitest": "^1.5.0",
"valibot": "^0.36.0",
"valibot30": "npm:valibot@0.30.0",
"valibot31": "npm:valibot@0.31.0",
"vitest": "^2.0.3",
"vitest-environment-nuxt": "^1.0.0",
"vue-tsc": "^2.0.13",
"vue-tsc": "^2.0.26",
"yup": "^1.4.0",
"zod": "^3.22.4"
"zod": "^3.23.8"
},
"resolutions": {
"@nuxt/kit": "^3.11.2",
"@nuxt/schema": "3.11.2",
"tailwindcss": "^3.4.3",
"@headlessui/vue": "1.7.19",
"vue": "3.4.21"
"@nuxt/ui": "workspace:*",
"nuxt-component-meta": "0.6.4",
"vue-tsc": "2.0.26"
}
}

View File

@@ -1,5 +1,7 @@
export default defineNuxtConfig({
modules: [
'../src/module'
]
})
],
compatibilityDate: '2024-07-23'
})

14
playground/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"private": true,
"name": "@nuxt/ui-playground",
"type": "module",
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build",
"generate": "nuxi generate"
},
"dependencies": {
"@nuxt/ui": "latest",
"nuxt": "^3.12.4"
}
}

22201
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
packages:
- "docs"
- "./"
- "docs"
- "playground"

View File

@@ -1,5 +1,12 @@
{
"extends": [
"github>nuxt/renovate-config-nuxt"
],
"ignoreDeps": [
"@nuxt/module-builder",
"nuxt-component-meta",
"valibot30",
"valibot31",
"vue-tsc"
]
}

View File

@@ -1,14 +1,11 @@
import { createRequire } from 'node:module'
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin } from '@nuxt/kit'
import { defaultExtractor as createDefaultExtractor } from 'tailwindcss/lib/lib/defaultExtractor.js'
import { iconsPlugin, getIconCollections, type CollectionNames, type IconsPluginOptions } from '@egoist/tailwindcss-icons'
import { name, version } from '../package.json'
import createTemplates from './templates'
import { generateSafelist, excludeColors, customSafelistExtractor } from './colors'
import * as config from './runtime/ui.config'
import type { DeepPartial, Strategy } from './runtime/types/utils'
import installTailwind from './tailwind'
const defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } })
const _require = createRequire(import.meta.url)
const defaultColors = _require('tailwindcss/colors.js')
@@ -24,7 +21,7 @@ type UI = {
colors?: string[]
strategy?: Strategy
[key: string]: any
} & DeepPartial<typeof config>
} & DeepPartial<typeof config, string>
declare module 'nuxt/schema' {
interface AppConfigInput {
@@ -50,8 +47,6 @@ export interface ModuleOptions {
*/
global?: boolean
icons: CollectionNames[] | 'all' | IconsPluginOptions
safelistColors?: string[]
/**
* Disables the global css styles added by the module.
@@ -65,12 +60,11 @@ export default defineNuxtModule<ModuleOptions>({
version,
configKey: 'ui',
compatibility: {
nuxt: '^3.10.0'
nuxt: '>=3.10.0'
}
},
defaults: {
prefix: 'U',
icons: ['heroicons'],
safelistColors: ['primary'],
disableGlobalStyles: false
},
@@ -88,107 +82,13 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.css.push(resolve(runtimeDir, 'ui.css'))
}
// @ts-ignore
nuxt.hook('tailwindcss:config', function (tailwindConfig) {
tailwindConfig.theme = tailwindConfig.theme || {}
tailwindConfig.theme.extend = tailwindConfig.theme.extend || {}
tailwindConfig.theme.extend.colors = tailwindConfig.theme.extend.colors || {}
const globalColors: any = {
...(tailwindConfig.theme.colors || defaultColors),
...tailwindConfig.theme.extend?.colors
}
// @ts-ignore
globalColors.primary = tailwindConfig.theme.extend.colors.primary = {
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
100: 'rgb(var(--color-primary-100) / <alpha-value>)',
200: 'rgb(var(--color-primary-200) / <alpha-value>)',
300: 'rgb(var(--color-primary-300) / <alpha-value>)',
400: 'rgb(var(--color-primary-400) / <alpha-value>)',
500: 'rgb(var(--color-primary-500) / <alpha-value>)',
600: 'rgb(var(--color-primary-600) / <alpha-value>)',
700: 'rgb(var(--color-primary-700) / <alpha-value>)',
800: 'rgb(var(--color-primary-800) / <alpha-value>)',
900: 'rgb(var(--color-primary-900) / <alpha-value>)',
950: 'rgb(var(--color-primary-950) / <alpha-value>)',
DEFAULT: 'rgb(var(--color-primary-DEFAULT) / <alpha-value>)'
}
if (globalColors.gray) {
// @ts-ignore
globalColors.cool = tailwindConfig.theme.extend.colors.cool = defaultColors.gray
}
// @ts-ignore
globalColors.gray = tailwindConfig.theme.extend.colors.gray = {
50: 'rgb(var(--color-gray-50) / <alpha-value>)',
100: 'rgb(var(--color-gray-100) / <alpha-value>)',
200: 'rgb(var(--color-gray-200) / <alpha-value>)',
300: 'rgb(var(--color-gray-300) / <alpha-value>)',
400: 'rgb(var(--color-gray-400) / <alpha-value>)',
500: 'rgb(var(--color-gray-500) / <alpha-value>)',
600: 'rgb(var(--color-gray-600) / <alpha-value>)',
700: 'rgb(var(--color-gray-700) / <alpha-value>)',
800: 'rgb(var(--color-gray-800) / <alpha-value>)',
900: 'rgb(var(--color-gray-900) / <alpha-value>)',
950: 'rgb(var(--color-gray-950) / <alpha-value>)'
}
const colors = excludeColors(globalColors)
// @ts-ignore
nuxt.options.appConfig.ui = {
primary: 'green',
gray: 'cool',
colors,
strategy: 'merge'
}
tailwindConfig.safelist = tailwindConfig.safelist || []
tailwindConfig.safelist.push(...generateSafelist(options.safelistColors || [], colors))
})
createTemplates(nuxt)
// Modules
await installModule('nuxt-icon')
await installModule('@nuxt/icon', { componentName: 'UIcon' })
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
await installModule('@nuxtjs/tailwindcss', {
exposeConfig: true,
config: {
darkMode: 'class',
plugins: [
require('@tailwindcss/forms')({ strategy: 'class' }),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/typography'),
require('@tailwindcss/container-queries'),
require('@headlessui/tailwindcss'),
iconsPlugin(Array.isArray(options.icons) || options.icons === 'all' ? { collections: getIconCollections(options.icons) } : typeof options.icons === 'object' ? options.icons as IconsPluginOptions : {})
],
content: {
files: [
resolve(runtimeDir, 'components/**/*.{vue,mjs,ts}'),
resolve(runtimeDir, 'ui.config/**/*.{mjs,js,ts}')
],
transform: {
vue: (content) => {
return content.replaceAll(/(?:\r\n|\r|\n)/g, ' ')
}
},
extract: {
vue: (content) => {
return [
...defaultExtractor(content),
// @ts-ignore
...customSafelistExtractor(options.prefix, content, nuxt.options.appConfig.ui.colors, options.safelistColors)
]
}
}
}
}
})
await installTailwind(options, nuxt, resolve)
// Plugins

View File

@@ -1,13 +1,28 @@
<template>
<div :class="ui.wrapper" v-bind="attrs">
<table :class="[ui.base, ui.divide]">
<slot v-if="$slots.caption || caption" name="caption">
<caption :class="ui.caption">
{{ caption }}
</caption>
</slot>
<thead :class="ui.thead">
<tr :class="ui.tr.base">
<th v-if="modelValue" scope="col" :class="ui.checkbox.padding">
<UCheckbox :model-value="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" v-bind="ui.default.checkbox" aria-label="Select all" @change="onChange" />
</th>
<th v-for="(column, index) in columns" :key="index" scope="col" :class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size, column.class]">
<th v-if="$slots.expand" scope="col" :class="ui.tr.base">
<span class="sr-only">Expand</span>
</th>
<th
v-for="(column, index) in columns"
:key="index"
scope="col"
:class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size, column.class]"
:aria-sort="getAriaSort(column)"
>
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
<UButton
v-if="column.sortable"
@@ -55,17 +70,39 @@
</tr>
<template v-else>
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected, $attrs.onSelect && ui.tr.active, row?.class]" @click="() => onSelect(row)">
<td v-if="modelValue" :class="ui.checkbox.padding">
<UCheckbox v-model="selected" :value="row" v-bind="ui.default.checkbox" aria-label="Select row" @click.stop />
</td>
<template v-for="(row, index) in rows" :key="index">
<tr :class="[ui.tr.base, isSelected(row) && ui.tr.selected, $attrs.onSelect && ui.tr.active, row?.class]" @click="() => onSelect(row)">
<td v-if="modelValue" :class="ui.checkbox.padding">
<UCheckbox v-model="selected" :value="row" v-bind="ui.default.checkbox" aria-label="Select row" @click.stop />
</td>
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size, row[column.key]?.class]">
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)">
{{ getRowData(row, column.key) }}
</slot>
</td>
</tr>
<td
v-if="$slots.expand"
:class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]"
>
<UButton
v-bind="{ ...(ui.default.expandButton || {}), ...expandButton }"
:ui="{ icon: { base: [ui.expand.icon, openedRows.includes(index) && 'rotate-180'] } }"
@click="toggleOpened(index)"
/>
</td>
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size, column?.rowClass, row[column.key]?.class]">
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)">
{{ getRowData(row, column.key) }}
</slot>
</td>
</tr>
<tr v-if="openedRows.includes(index)">
<td colspan="100%">
<slot
name="expand"
:row="row"
:index="index"
/>
</td>
</tr>
</template>
</template>
</tbody>
</table>
@@ -73,15 +110,12 @@
</template>
<script lang="ts">
import { computed, defineComponent, toRaw, toRef } from 'vue'
import type { PropType } from 'vue'
import { ref, computed, defineComponent, toRaw, toRef } from 'vue'
import type { PropType, AriaAttributes } from 'vue'
import { upperFirst } from 'scule'
import { defu } from 'defu'
import { useVModel } from '@vueuse/core'
import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import UProgress from '../elements/Progress.vue'
import UCheckbox from '../forms/Checkbox.vue'
import { UIcon, UButton, UProgress, UCheckbox } from '#components'
import { useUI } from '../../composables/useUI'
import { mergeConfig, get } from '../../utils'
import type { Strategy, Button, ProgressColor, ProgressAnimation } from '../../types'
@@ -107,6 +141,16 @@ function defaultSort (a: any, b: any, direction: 'asc' | 'desc') {
}
}
interface Column {
key: string
sortable?: boolean
sort?: (a: any, b: any, direction: 'asc' | 'desc') => number
direction?: 'asc' | 'desc'
class?: string
rowClass?: string
[key: string]: any
}
export default defineComponent({
components: {
UIcon,
@@ -129,7 +173,7 @@ export default defineComponent({
default: () => []
},
columns: {
type: Array as PropType<{ key: string, sortable?: boolean, sort?: (a: any, b: any, direction: 'asc' | 'desc') => number, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>,
type: Array as PropType<Column[]>,
default: null
},
columnAttribute: {
@@ -156,6 +200,10 @@ export default defineComponent({
type: String,
default: () => config.default.sortDescIcon
},
expandButton: {
type: Object as PropType<Button>,
default: () => config.default.expandButton as Button
},
loading: {
type: Boolean,
default: false
@@ -168,6 +216,10 @@ export default defineComponent({
type: Object as PropType<{ icon: string, label: string }>,
default: () => config.default.emptyState
},
caption: {
type: String,
default: null
},
progress: {
type: Object as PropType<{ color: ProgressColor, animation: ProgressAnimation }>,
default: () => config.default.progress
@@ -185,10 +237,12 @@ export default defineComponent({
setup (props, { emit, attrs: $attrs }) {
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class'))
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: upperFirst(key), sortable: false, class: undefined, sort: defaultSort })))
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: upperFirst(key), sortable: false, class: undefined, sort: defaultSort }) as Column))
const sort = useVModel(props, 'sort', emit, { passive: true, defaultValue: defu({}, props.sort, { column: null, direction: 'asc' }) })
const openedRows = ref([])
const savedSort = { column: sort.value.column, direction: null }
const rows = computed(() => {
@@ -292,6 +346,34 @@ export default defineComponent({
return get(row, rowKey, defaultValue)
}
function toggleOpened (index: number) {
if (openedRows.value.includes(index)) {
openedRows.value = openedRows.value.filter((i) => i !== index)
} else {
openedRows.value.push(index)
}
}
function getAriaSort (column: Column): AriaAttributes['aria-sort'] {
if (!column.sortable) {
return undefined
}
if (sort.value.column !== column.key) {
return 'none'
}
if (sort.value.direction === 'asc') {
return 'ascending'
}
if (sort.value.direction === 'desc') {
return 'descending'
}
return undefined
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
@@ -308,11 +390,14 @@ export default defineComponent({
emptyState,
// eslint-disable-next-line vue/no-dupe-keys
loadingState,
openedRows,
isSelected,
onSort,
onSelect,
onChange,
getRowData
getRowData,
toggleOpened,
getAriaSort
}
}
})

View File

@@ -69,8 +69,7 @@
import { ref, computed, toRef, defineComponent, watch } from 'vue'
import type { PropType } from 'vue'
import { Disclosure as HDisclosure, DisclosureButton as HDisclosureButton, DisclosurePanel as HDisclosurePanel, provideUseId } from '@headlessui/vue'
import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import { UIcon, UButton } from '#components'
import { useUI } from '../../composables/useUI'
import { mergeConfig, omit } from '../../utils'
import type { AccordionItem, Strategy } from '../../types'
@@ -133,7 +132,7 @@ export default defineComponent({
const uiButton = computed<typeof configButton>(() => configButton)
const buttonRefs = ref<{ open: boolean, close: (e: EventTarget) => {} }[]>([])
const openedStates = computed(() => buttonRefs.value.map(({ open }) => open))
watch(openedStates, (newValue, oldValue) => {
for (const index in newValue) {

View File

@@ -2,7 +2,7 @@
<div :class="alertClass" v-bind="attrs">
<div class="flex" :class="[ui.gap, { 'items-start': (description || $slots.description), 'items-center': !description && !$slots.description }]">
<slot name="icon" :icon="icon">
<UIcon v-if="icon" :name="icon" :ui="ui.icon.base" />
<UIcon v-if="icon" :name="icon" :class="ui.icon.base" />
</slot>
<slot name="avatar" :avatar="avatar">
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
@@ -14,19 +14,23 @@
{{ title }}
</slot>
</p>
<p v-if="description || $slots.description" :class="twMerge(ui.description, !(title && $slots.title) && 'mt-0 leading-5')">
<div v-if="description || $slots.description" :class="twMerge(ui.description, !title && !$slots.title && 'mt-0 leading-5')">
<slot name="description" :description="description">
{{ description }}
</slot>
</p>
</div>
<div v-if="(description || $slots.description) && actions.length" :class="ui.actions">
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
<div v-if="(description || $slots.description) && (actions.length || $slots.actions)" :class="ui.actions">
<slot name="actions">
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
</slot>
</div>
</div>
<div v-if="closeButton || (!description && !$slots.description && actions.length)" :class="twMerge(ui.actions, 'mt-0')">
<template v-if="!description && !$slots.description && actions.length">
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
<template v-if="!description && !$slots.description && (actions.length || $slots.actions)">
<slot name="actions">
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
</slot>
</template>
<UButton v-if="closeButton" aria-label="Close" v-bind="{ ...(ui.default.closeButton || {}), ...closeButton }" @click.stop="$emit('close')" />
@@ -39,9 +43,7 @@
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue'
import { UIcon, UAvatar, UButton } from '#components'
import { useUI } from '../../composables/useUI'
import type { Avatar, Button, AlertColor, AlertVariant, AlertAction, Strategy } from '../../types'
import { mergeConfig } from '../../utils'

View File

@@ -24,7 +24,7 @@
import { defineComponent, ref, computed, toRef, watch } from 'vue'
import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { UIcon } from '#components'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { AvatarSize, AvatarChipColor, AvatarChipPosition, Strategy } from '../../types'

View File

@@ -20,8 +20,7 @@
import { computed, defineComponent, toRef } from 'vue'
import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import ULink from '../elements/Link.vue'
import { UIcon, ULink } from '#components'
import { useUI } from '../../composables/useUI'
import { mergeConfig, nuxtLinkProps, getNuxtLinkProps } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup'

View File

@@ -61,9 +61,7 @@ import type { PropType } from 'vue'
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem, provideUseId } from '@headlessui/vue'
import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue'
import { UIcon, UAvatar, UKbd } from '#components'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { mergeConfig, getNuxtLinkProps } from '../../utils'

View File

@@ -1,33 +0,0 @@
<template>
<Icon v-if="dynamic" :name="name" />
<span v-else :class="name" />
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useAppConfig } from '#imports'
export default defineComponent({
props: {
name: {
type: String,
required: true
},
dynamic: {
type: Boolean,
default: false
}
},
setup (props) {
const appConfig = useAppConfig()
// @ts-ignore
const dynamic = computed(() => props.dynamic || appConfig.ui?.icons?.dynamic)
return {
// eslint-disable-next-line vue/no-dupe-keys
dynamic
}
}
})
</script>

View File

@@ -31,7 +31,7 @@
import { computed, defineComponent, toRef } from 'vue'
import type { SlotsType, PropType } from 'vue'
import { twJoin } from 'tailwind-merge'
import UIcon from './Icon.vue'
import { UIcon } from '#components'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy, MeterColor, MeterSize } from '../../types'

View File

@@ -1,7 +1,7 @@
import { h, cloneVNode, computed, toRef, defineComponent } from 'vue'
import type { ComputedRef, VNode, SlotsType, PropType } from 'vue'
import { twJoin } from 'tailwind-merge'
import UIcon from './Icon.vue'
import { UIcon } from '#components'
import Meter from './Meter.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig, getSlotsChildren } from '../../utils'

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper" v-bind="attrs">
<div :class="ui.wrapper" role="progressbar">
<slot v-if="indicator || $slots.indicator" name="indicator" v-bind="{ percent }">
<div v-if="!isSteps" :class="indicatorContainerClass" :style="{ width: `${percent}%` }">
<div :class="indicatorClass">
@@ -8,7 +8,12 @@
</div>
</slot>
<progress :class="progressClass" v-bind="{ value, max: realMax }">
<progress
:aria-valuemax="realMax"
:aria-valuenow="value"
:class="progressClass"
v-bind="{ value, max: realMax, ...attrs }"
>
{{ percent !== undefined ? `${Math.round(percent)}%` : undefined }}
</progress>

View File

@@ -17,11 +17,13 @@
</div>
<div v-if="label || $slots.label" :class="ui.inner">
<label :for="inputId" :class="ui.label">
<slot name="label">{{ label }}</slot>
<slot name="label" :label="label">{{ label }}</slot>
<span v-if="required" :class="ui.required">*</span>
</label>
<p v-if="help" :class="ui.help">
{{ help }}
<p v-if="help || $slots.help" :class="ui.help">
<slot name="help" :help="help">
{{ help }}
</slot>
</p>
</div>
</div>

View File

@@ -10,7 +10,9 @@ import { useEventBus } from '@vueuse/core'
import type { ZodSchema } from 'zod'
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
import type { ObjectSchemaAsync as ValibotObjectSchema } from 'valibot'
import type { BaseSchema as ValibotSchema30, BaseSchemaAsync as ValibotSchemaAsync30 } from 'valibot30'
import type { GenericSchema as ValibotSchema31, GenericSchemaAsync as ValibotSchemaAsync31, SafeParser as ValibotSafeParser31, SafeParserAsync as ValibotSafeParserAsync31 } from 'valibot31'
import type { GenericSchema as ValibotSchema, GenericSchemaAsync as ValibotSchemaAsync, SafeParser as ValibotSafeParser, SafeParserAsync as ValibotSafeParserAsync } from 'valibot'
import type { FormError, FormEvent, FormEventType, FormSubmitEvent, FormErrorEvent, Form } from '../../types/form'
import { useId } from '#imports'
@@ -25,11 +27,15 @@ class FormException extends Error {
export default defineComponent({
props: {
schema: {
type: Object as
type: [Object, Function] as
| PropType<ZodSchema>
| PropType<YupObjectSchema<any>>
| PropType<JoiSchema>
| PropType<ValibotObjectSchema<any>>,
| PropType<ValibotSchema30 | ValibotSchemaAsync30>
| PropType<ValibotSchema31 | ValibotSchemaAsync31>
| PropType<ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any>>
| PropType<ValibotSchema | ValibotSchemaAsync>
| PropType<ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>>,
default: undefined
},
state: {
@@ -151,7 +157,6 @@ export default defineComponent({
validate,
errors,
setErrors (errs: FormError[], path?: string) {
errors.value = errs
if (path) {
errors.value = errors.value.filter(
(error) => error.path !== path
@@ -256,21 +261,19 @@ async function getJoiErrors (
}
}
function isValibotSchema (schema: any): schema is ValibotObjectSchema<any> {
return schema._parse !== undefined
function isValibotSchema (schema: any): schema is ValibotSchema30 | ValibotSchemaAsync30 | ValibotSchema31 | ValibotSchemaAsync31 | ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any> | ValibotSchema | ValibotSchemaAsync | ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any> {
return '_parse' in schema || '_run' in schema || (typeof schema === 'function' && 'schema' in schema)
}
async function getValibotError (
state: any,
schema: ValibotObjectSchema<any>
schema: ValibotSchema30 | ValibotSchemaAsync30 | ValibotSchema31 | ValibotSchemaAsync31 | ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any> | ValibotSchema | ValibotSchemaAsync | ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>
): Promise<FormError[]> {
const result = await schema._parse(state)
if (result.issues) {
return result.issues.map((issue) => ({
path: issue.path?.map(p => p.key).join('.') || '',
message: issue.message
}))
}
return []
const result = await ('_parse' in schema ? schema._parse(state) : '_run' in schema ? schema._run({ typed: false, value: state }, {}) : schema(state))
return result.issues?.map((issue) => ({
// We know that the key for a form schema is always a string or a number
path: issue.path?.map((item) => item.key).join('.') || '',
message: issue.message
})) || []
}
</script>

View File

@@ -23,7 +23,7 @@
<div :class="[label ? ui.container : '']">
<slot v-bind="{ error }" />
<p v-if="(typeof error === 'string' && error) || $slots.error" :class="[ui.error, size]">
<p v-if="typeof error === 'string' && error" :class="[ui.error, size]">
<slot v-if="$slots.error" name="error" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>
{{ error }}

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper">
<div :class="(type === 'hidden') ? 'hidden' : ui.wrapper">
<input
:id="inputId"
ref="input"
@@ -35,7 +35,7 @@
import { ref, computed, toRef, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { UIcon } from '#components'
import { defu } from 'defu'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
@@ -171,7 +171,7 @@ export default defineComponent({
const { emitFormBlur, emitFormInput, size: sizeFormGroup, color, inputId, name } = useFormGroup(props, config)
const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value)
const size = computed(() => sizeButtonGroup.value ?? sizeFormGroup.value)
const modelModifiers = ref(defu({}, props.modelModifiers, { trim: false, lazy: false, number: false }))
@@ -240,7 +240,7 @@ export default defineComponent({
ui.value.form,
rounded.value,
ui.value.placeholder,
props.type === 'file' && [ui.value.file.base, ui.value.file.padding[size.value]],
props.type === 'file' && ui.value.file.base,
ui.value.size[size.value],
props.padded ? ui.value.padding[size.value] : 'p-0',
variant?.replaceAll('{color}', color.value),

View File

@@ -104,8 +104,7 @@ import {
import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import { UIcon, UAvatar } from '#components'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { useFormGroup } from '../../composables/useFormGroup'
@@ -249,6 +248,10 @@ export default defineComponent({
type: Array,
default: null
},
searchLazy: {
type: Boolean,
default: false
},
debounce: {
type: Number,
default: 200
@@ -286,7 +289,7 @@ export default defineComponent({
const { size: sizeButtonGroup, rounded } = useInjectButtonGroup({ ui, props })
const { emitFormBlur, emitFormChange, inputId, color, size: sizeFormGroup, name } = useFormGroup(props, config)
const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value)
const size = computed(() => sizeButtonGroup.value ?? sizeFormGroup.value)
const internalQuery = ref('')
const query = computed({
@@ -305,7 +308,7 @@ export default defineComponent({
}
if (props.valueAttribute) {
const option = props.options.find(option => option[props.valueAttribute] === props.modelValue)
const option = options.value.find(option => option[props.valueAttribute] === props.modelValue)
return option ? option[props.optionAttribute] : null
} else {
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : props.modelValue[props.optionAttribute]
@@ -387,16 +390,22 @@ export default defineComponent({
const debouncedSearch = props.search && typeof props.search === 'function' ? useDebounceFn(props.search, props.debounce) : undefined
const filteredOptions = computedAsync(async () => {
const options = computedAsync(async () => {
if (debouncedSearch) {
return await debouncedSearch(query.value)
}
if (query.value === '') {
return props.options
return props.options || []
}, [], {
lazy: props.searchLazy
})
const filteredOptions = computed(() => {
if (!query.value || debouncedSearch) {
return options.value
}
return (props.options as any[]).filter((option: any) => {
return options.value.filter((option: any) => {
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => {
if (['string', 'number'].includes(typeof option)) {
return String(option).search(new RegExp(query.value, 'i')) !== -1

View File

@@ -16,11 +16,13 @@
</div>
<div v-if="label || $slots.label" :class="ui.inner">
<label :for="inputId" :class="ui.label">
<slot name="label">{{ label }}</slot>
<slot name="label" :label="label">{{ label }}</slot>
<span v-if="required" :class="ui.required">*</span>
</label>
<p v-if="help" :class="ui.help">
{{ help }}
<p v-if="help || $slots.help" :class="ui.help">
<slot name="help" :help="help">
{{ help }}
</slot>
</p>
</div>
</div>

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