Compare commits

...

63 Commits

Author SHA1 Message Date
Benjamin Canac
159acd664c chore(release): v2.19.2 2024-11-05 19:36:54 +01:00
Benjamin Canac
212f7df35b fix(Button): put back target override 2024-11-05 19:26:37 +01:00
Benjamin Canac
d0d37a06d2 chore(release): v2.19.1 2024-11-05 18:06:03 +01:00
Benjamin Canac
cb6f5f2d71 fix(InputMenu/SelectMenu): regex breaks build 2024-11-05 17:57:49 +01:00
Benjamin Canac
22da1a839a docs(date-picker): improve component
Resolves #2082
2024-11-05 17:35:49 +01:00
Benjamin Canac
c5f76a25db chore(release): v2.19.0 2024-11-05 16:56:42 +01:00
kyyy
ceecb60c3b feat(Form): apply transformations (#2460) 2024-11-05 16:13:25 +01:00
CJBoy
23971efdb0 fix(module): missing types in ui config (#2467) 2024-11-05 16:08:54 +01:00
Ersan Karimi
1a94b55caa fix(InputMenu/SelectMenu): prevent unnecessary updates when modelValue is unchanged (#2507) 2024-11-05 16:08:36 +01:00
offich
c71fdc8795 feat(Pagination): improve slot props (#2522) 2024-11-05 16:06:49 +01:00
renovate[bot]
6844f7bbd9 chore(deps): update all non-major dependencies (dev) (#2525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 16:06:36 +01:00
kyyy
1acd01a440 feat(Table): improve expanded row (#2485) 2024-11-05 15:52:10 +01:00
Benjamin Canac
0b2a3989a2 chore(deps): dedupe 2024-11-05 15:18:24 +01:00
renovate[bot]
5f8d645231 chore(deps): update nuxt framework to ^3.14.0 (dev) (#2526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 19:39:53 +01:00
renovate[bot]
2cc838ea8b chore(deps): lock file maintenance (dev) (#2519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 14:20:15 +01:00
Benjamin Canac
2e41e3f238 docs(table): fix expandable example responsive 2024-11-04 14:14:59 +01:00
renovate[bot]
7cb8218ed5 chore(deps): update devdependency eslint to ^9.14.0 (dev) (#2514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 10:28:55 +01:00
Eder Soares
ddf67a060b feat(InputMenu): allows to customize labels (#2295) 2024-10-31 15:29:08 +01:00
Eder Soares
54e713d31a feat(SelectMenu): allows to customize labels (#2266) 2024-10-31 15:17:08 +01:00
renovate[bot]
09e232ed05 chore(deps): update dependency @nuxt/ui-pro to v1.4.4-28839576.e8eba4f (dev) (#2504)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 14:19:17 +01:00
renovate[bot]
1d455b092d chore(deps): update all non-major dependencies to ^11.2.0 (dev) (#2496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 12:34:00 +01:00
Benjamin Canac
13957ba206 chore(deps): remove vue-tsc resolutions 2024-10-31 11:32:01 +01:00
kyyy
ff1806143c fix(InputMenu/SelectMenu): allow access nested object in option-attribute (#2465)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-10-30 17:33:06 +01:00
Nestor Vera
b6ed1c59ff fix(RadioGroup): rendering empty slots (#2456) 2024-10-30 12:42:56 +01:00
renovate[bot]
424efe783e chore(deps): lock file maintenance (dev) (#2473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 12:08:56 +01:00
renovate[bot]
c3cd3c9940 chore(deps): update all non-major dependencies (dev) (#2453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-10-30 11:51:25 +01:00
Benjamin Canac
8ab4a14394 fix(Button): wrong to type
Resolves #1253
2024-10-30 11:16:04 +01:00
Mateus Bellei
25378df1d8 fix(Accordion): improve items type (#2487) 2024-10-29 16:38:59 +01:00
kyyy
070d2f89b6 fix(Table): indeterminate checkbox with pagination (#2439)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-10-24 12:41:45 +02:00
renovate[bot]
8e413f0681 chore(deps): update dependency @nuxt/ui-pro to v1.4.4-28829363.bb3c738 (dev) (#2444)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 12:32:52 +02:00
Benjamin Canac
03ac697167 docs(deps): remove eslint dependency 2024-10-24 11:16:52 +02:00
renovate[bot]
c6a9b499e3 chore(deps): update dependency eslint to v9 (dev) (#2446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 11:14:38 +02:00
Benjamin Canac
cae4f0c4a8 chore(deps): migrate to eslint 9 (#2443) 2024-10-24 10:30:37 +02:00
renovate[bot]
b29fcd2650 chore(deps): update all non-major dependencies (dev) (#2411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 21:50:02 +02:00
Benjamin Canac
3671b2fbbe chore(renovate): ignore resolutions 2024-10-23 21:15:17 +02:00
renovate[bot]
2577eb2780 chore(deps): update devdependency @nuxt/test-utils to ^3.14.4 (dev) (#2422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 18:21:53 +02:00
renovate[bot]
3d1be39221 chore(deps): lock file maintenance (dev) (#2427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 18:13:04 +02:00
Benjamin Canac
49e04389fa chore(deps): set @nuxt/content & @nuxtjs/mdc resolutions 2024-10-21 11:37:48 +02:00
Benjamin Canac
ee364318d1 docs: use parseMarkdown instead of transformContent 2024-10-21 11:25:41 +02:00
Benjamin Canac
b14afbebe9 docs(prettier): update usage 2024-10-21 10:55:10 +02:00
Malik-Jouda
4bf81be364 fix(HorizontalNavigation/VerticalNavigation): handle badge in RTL mode (#2420) 2024-10-19 19:36:00 +02:00
Benjamin Canac
7846ca35b5 fix(Divider): default type from app config
Resolves nuxt/ui#2398
2024-10-19 14:19:17 +02:00
rizkyyy
b72d3434e9 fix(Table): handle dot nation with by prop (#2413)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-10-19 12:29:06 +02:00
Malik-Jouda
20fb46a3ba fix(Progress): handle carousel and carousel-inverse animations in RTL mode (#2400)
Co-authored-by: malik jouda <m.jouda@approved.tech>
2024-10-17 22:28:19 +02:00
rizkyyy
1b7e36cf70 fix(Table): checkbox not checked while using props by (#2401) 2024-10-17 22:21:19 +02:00
renovate[bot]
3768cd9803 chore(deps): update all non-major dependencies (dev) (#2394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 22:15:05 +02:00
Halil Durak
3d0bba2e83 chore(module): call only a single await to install tailwind (#2397) 2024-10-17 14:46:49 +02:00
Benjamin Canac
494e73932b docs(deps): update @nuxt/ui-pro 2024-10-17 12:31:12 +02:00
renovate[bot]
38200aa392 chore(deps): update all non-major dependencies (dev) (#2381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-10-15 17:24:07 +02:00
renovate[bot]
19b01f43f1 chore(deps): update dependency tailwindcss to ^3.4.14 (dev) (#2387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 16:51:35 +02:00
Benjamin Canac
c36964b5ea fix(Table): export TableRow and TableColumn types
Resolves nuxt/ui#2373
2024-10-15 11:05:08 +02:00
renovate[bot]
4de8f2e2f7 chore(deps): update all non-major dependencies (dev) (#2380)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 17:25:41 +02:00
Benjamin Canac
3cf19ea5af fix(Tabs): allow aria-label on items
Related to nuxt/ui#1934
2024-10-14 16:18:33 +02:00
Gerben Mulder
9dd7e615e9 feat(Input/Textarea): nullify model modifier (#2309) 2024-10-14 12:45:15 +02:00
renovate[bot]
33b9a445c4 chore(deps): update devdependency @release-it/conventional-changelog to v9 (dev) (#2367)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 11:05:06 +02:00
renovate[bot]
46cec7ecd1 chore(deps): update all non-major dependencies (dev) (#2351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 11:00:32 +02:00
renovate[bot]
f8e2c94375 chore(deps): lock file maintenance (dev) (#2375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 10:44:13 +02:00
Benjamin Canac
71e0492179 chore(github): add version select 2024-10-12 19:12:14 +02:00
rizkyyy
3cda6c6478 feat(Form): add superstruct validation (#2357) 2024-10-11 11:02:51 +02:00
Benjamin Canac
428ee44fc0 docs(app): add v3-alpha banner 2024-10-10 18:08:39 +02:00
Benjamin Canac
c68ba76fd0 fix(InputMenu/SelectMenu): escape regexp before search
Resolves nuxt/ui#2308
2024-10-10 16:12:42 +02:00
Benjamin Canac
dd0d0551be docs(notification): improve position override example
nuxt/ui#2128
2024-10-10 11:45:19 +02:00
renovate[bot]
3efcf3026a chore(deps): update dependency @nuxt/ui-pro to ^1.4.4 (dev) (#2348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-09 17:48:41 +02:00
205 changed files with 4558 additions and 4443 deletions

View File

@@ -1,14 +0,0 @@
node_modules
dist
.nuxt
coverage
*.log*
.DS_Store
.code
*.iml
package-lock.json
templates/*
sw.js
# Templates
src/templates

View File

@@ -1,46 +0,0 @@
module.exports = {
root: true,
extends: ['@nuxt/eslint-config'],
rules: {
// General
semi: ['error', 'never'],
quotes: ['error', 'single'],
'comma-dangle': ['error', 'never'],
'comma-spacing': ['error', { before: false, after: true }],
'keyword-spacing': ['error', { before: true, after: true }],
'space-before-function-paren': ['error', 'always'],
'object-curly-spacing': ['error', 'always'],
'arrow-spacing': ['error', { before: true, after: true }],
'key-spacing': ['error', { beforeColon: false, afterColon: true, mode: 'strict' }],
'space-before-blocks': ['error', 'always'],
'space-infix-ops': ['error', { int32Hint: false }],
'no-multi-spaces': ['error', { ignoreEOLComments: true }],
'no-trailing-spaces': ['error'],
// Typescript
'@typescript-eslint/type-annotation-spacing': 'error',
// Vuejs
'vue/multi-word-component-names': 0,
'vue/html-indent': ['error', 2],
'vue/comma-spacing': ['error', { before: false, after: true }],
'vue/script-indent': ['error', 2, { baseIndent: 0 }],
'vue/keyword-spacing': ['error', { before: true, after: true }],
'vue/object-curly-spacing': ['error', 'always'],
'vue/key-spacing': ['error', { beforeColon: false, afterColon: true, mode: 'strict' }],
'vue/arrow-spacing': ['error', { before: true, after: true }],
'vue/array-bracket-spacing': ['error', 'never'],
'vue/block-spacing': ['error', 'always'],
'vue/brace-style': ['error', 'stroustrup', { allowSingleLine: true }],
'vue/space-infix-ops': ['error', { int32Hint: false }],
'vue/max-attributes-per-line': [
'error',
{
singleline: {
max: 5
}
}
],
'vue/padding-line-between-blocks': ['error', 'always']
}
}

View File

@@ -6,6 +6,15 @@ body:
attributes: attributes:
value: | value: |
Before requesting a feature, please make sure that you have read through our [documentation](https://ui.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc). Before requesting a feature, please make sure that you have read through our [documentation](https://ui.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
- type: dropdown
id: version
attributes:
label: For what version of Nuxt UI are you suggesting this?
options:
- v2.x
- v3-alpha
validations:
required: true
- type: textarea - type: textarea
id: description id: description
attributes: attributes:

View File

@@ -6,6 +6,15 @@ body:
attributes: attributes:
value: | value: |
Before asking a question, please make sure that you have read through our [documentation](https://ui.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc). Before asking a question, please make sure that you have read through our [documentation](https://ui.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
- type: dropdown
id: version
attributes:
label: For what version of Nuxt UI are you asking this question?
options:
- v2.x
- v3-alpha
validations:
required: true
- type: textarea - type: textarea
id: description id: description
attributes: attributes:

View File

@@ -1,5 +1,47 @@
# Changelog # Changelog
## [2.19.2](https://github.com/nuxt/ui/compare/v2.19.1...v2.19.2) (2024-11-05)
### Bug Fixes
* **Button:** put back `target` override ([212f7df](https://github.com/nuxt/ui/commit/212f7df35b9f81d189e1ee3e34f6fd2234cf52fe))
## [2.19.1](https://github.com/nuxt/ui/compare/v2.19.0...v2.19.1) (2024-11-05)
### Bug Fixes
* **InputMenu/SelectMenu:** regex breaks build ([cb6f5f2](https://github.com/nuxt/ui/commit/cb6f5f2d71ea8bb526a8f958daec8e9871469b63))
## [2.19.0](https://github.com/nuxt/ui/compare/v2.18.7...v2.19.0) (2024-11-05)
### Features
* **Form:** add `superstruct` validation ([#2357](https://github.com/nuxt/ui/issues/2357)) ([3cda6c6](https://github.com/nuxt/ui/commit/3cda6c6478d5284a3ffcb973270831601e8e5657))
* **Form:** apply transformations ([#2460](https://github.com/nuxt/ui/issues/2460)) ([ceecb60](https://github.com/nuxt/ui/commit/ceecb60c3bbd5507b1f54faed001818639d9269c))
* **Input/Textarea:** nullify model modifier ([#2309](https://github.com/nuxt/ui/issues/2309)) ([9dd7e61](https://github.com/nuxt/ui/commit/9dd7e615e97b6bf3c4c4096edd35a86ca3cfd53c))
* **InputMenu:** allows to customize labels ([#2295](https://github.com/nuxt/ui/issues/2295)) ([ddf67a0](https://github.com/nuxt/ui/commit/ddf67a060ba659f102673eff31eb2e30231c2d93))
* **Pagination:** improve slot props ([#2522](https://github.com/nuxt/ui/issues/2522)) ([c71fdc8](https://github.com/nuxt/ui/commit/c71fdc8795812bed779ab247451efd3db031e4cd))
* **SelectMenu:** allows to customize labels ([#2266](https://github.com/nuxt/ui/issues/2266)) ([54e713d](https://github.com/nuxt/ui/commit/54e713d31ae0b80b0f69dd507f71387100204ac3))
* **Table:** improve `expanded` row ([#2485](https://github.com/nuxt/ui/issues/2485)) ([1acd01a](https://github.com/nuxt/ui/commit/1acd01a440db7a7fa765189d8bde424ade9074e9))
### Bug Fixes
* **Accordion:** improve `items` type ([#2487](https://github.com/nuxt/ui/issues/2487)) ([25378df](https://github.com/nuxt/ui/commit/25378df1d894546c4b08eb43a58b02b40ab9649b))
* **Button:** wrong `to` type ([8ab4a14](https://github.com/nuxt/ui/commit/8ab4a14394e0890b33a610e6491d891e89386959)), closes [#1253](https://github.com/nuxt/ui/issues/1253)
* **Divider:** default `type` from app config ([7846ca3](https://github.com/nuxt/ui/commit/7846ca35b5332a9e70f9990059f6041d60770e79)), closes [nuxt/ui#2398](https://github.com/nuxt/ui/issues/2398)
* **HorizontalNavigation/VerticalNavigation:** handle `badge` in RTL mode ([#2420](https://github.com/nuxt/ui/issues/2420)) ([4bf81be](https://github.com/nuxt/ui/commit/4bf81be36463bf280f31099c97a751e65240dcf5))
* **InputMenu/SelectMenu:** allow access nested object in `option-attribute` ([#2465](https://github.com/nuxt/ui/issues/2465)) ([ff18061](https://github.com/nuxt/ui/commit/ff1806143c45a7d83b00e78bec979a8f412a2827))
* **InputMenu/SelectMenu:** escape regexp before search ([c68ba76](https://github.com/nuxt/ui/commit/c68ba76fd0eebf411ccd5f047ee9a01b8ec5f5de)), closes [nuxt/ui#2308](https://github.com/nuxt/ui/issues/2308)
* **InputMenu/SelectMenu:** prevent unnecessary updates when modelValue is unchanged ([#2507](https://github.com/nuxt/ui/issues/2507)) ([1a94b55](https://github.com/nuxt/ui/commit/1a94b55caac91685f518ae4c24ca8dcbee827f86))
* **module:** missing types in `ui` config ([#2467](https://github.com/nuxt/ui/issues/2467)) ([23971ef](https://github.com/nuxt/ui/commit/23971efdb007701352ce58412db597cd95b9996b))
* **Progress:** handle `carousel` and `carousel-inverse` animations in RTL mode ([#2400](https://github.com/nuxt/ui/issues/2400)) ([20fb46a](https://github.com/nuxt/ui/commit/20fb46a3ba8d74fcaa1407b23d65b117cc9d6802))
* **RadioGroup:** rendering empty slots ([#2456](https://github.com/nuxt/ui/issues/2456)) ([b6ed1c5](https://github.com/nuxt/ui/commit/b6ed1c59ffe8c8aaac78a34d8559ca793bb92eaa))
* **Table:** `checkbox` not checked while using props by ([#2401](https://github.com/nuxt/ui/issues/2401)) ([1b7e36c](https://github.com/nuxt/ui/commit/1b7e36cf70a7252915c58657bc878cb29c719a7f))
* **Table:** `indeterminate` checkbox with pagination ([#2439](https://github.com/nuxt/ui/issues/2439)) ([070d2f8](https://github.com/nuxt/ui/commit/070d2f89b6d1cb9c236eeb779cb3918ed5770434))
* **Table:** export `TableRow` and `TableColumn` types ([c36964b](https://github.com/nuxt/ui/commit/c36964b5eacbd61a661f02953f0297a390fd1d34)), closes [nuxt/ui#2373](https://github.com/nuxt/ui/issues/2373)
* **Table:** handle dot nation with `by` prop ([#2413](https://github.com/nuxt/ui/issues/2413)) ([b72d343](https://github.com/nuxt/ui/commit/b72d3434e9ab024e8622611d32b5a4467c8364b9))
* **Tabs:** allow `aria-label` on items ([3cf19ea](https://github.com/nuxt/ui/commit/3cf19ea5afcf97ef226d8be231d3b297c5f23b9f)), closes [nuxt/ui#1934](https://github.com/nuxt/ui/issues/1934)
## [2.18.7](https://github.com/nuxt/ui/compare/v2.18.6...v2.18.7) (2024-10-09) ## [2.18.7](https://github.com/nuxt/ui/compare/v2.18.6...v2.18.7) (2024-10-09)

View File

@@ -3,7 +3,7 @@
<div> <div>
<NuxtLoadingIndicator /> <NuxtLoadingIndicator />
<!-- <Banner v-if="!$route.path.startsWith('/examples')" /> --> <Banner v-if="!$route.path.startsWith('/examples')" />
<Header v-if="!$route.path.startsWith('/examples')" :links="links" /> <Header v-if="!$route.path.startsWith('/examples')" :links="links" />
@@ -50,7 +50,8 @@ const links = computed(() => {
icon: 'i-heroicons-book-open', icon: 'i-heroicons-book-open',
to: '/getting-started', to: '/getting-started',
active: route.path.startsWith('/getting-started') || route.path.startsWith('/components') active: route.path.startsWith('/getting-started') || route.path.startsWith('/components')
}, ...(navigation.value.find(item => item._path === '/pro') ? [{ }, ...(navigation.value.find(item => item._path === '/pro')
? [{
label: 'Pro', label: 'Pro',
icon: 'i-heroicons-square-3-stack-3d', icon: 'i-heroicons-square-3-stack-3d',
to: '/pro', to: '/pro',
@@ -63,7 +64,8 @@ const links = computed(() => {
label: 'Templates', label: 'Templates',
icon: 'i-heroicons-computer-desktop', icon: 'i-heroicons-computer-desktop',
to: '/pro/templates' to: '/pro/templates'
}] : []), { }]
: []), {
label: 'Releases', label: 'Releases',
icon: 'i-heroicons-rocket-launch', icon: 'i-heroicons-rocket-launch',
to: '/releases' to: '/releases'

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const id = 'nuxt-ui-banner-1' const id = 'nuxt-ui-banner-2'
const to = '/pro/pricing' const to = 'https://ui3.nuxt.dev'
const hideBanner = () => { const hideBanner = () => {
localStorage.setItem(id, 'true') localStorage.setItem(id, 'true')
@@ -25,7 +25,14 @@ if (import.meta.server) {
<template> <template>
<div class="relative bg-primary hover:bg-primary/90 transition-[background] backdrop-blur z-50 app-banner"> <div class="relative bg-primary hover:bg-primary/90 transition-[background] backdrop-blur z-50 app-banner">
<UContainer class="py-2"> <UContainer class="py-2">
<NuxtLink v-if="to" :to="to" class="focus:outline-none" aria-label="Nuxt UI Pro pricing" tabindex="-1"> <NuxtLink
v-if="to"
:to="to"
target="_blank"
class="focus:outline-none"
aria-label="Nuxt UI Pro pricing"
tabindex="-1"
>
<span class="absolute inset-0 " aria-hidden="true" /> <span class="absolute inset-0 " aria-hidden="true" />
</NuxtLink> </NuxtLink>
@@ -34,9 +41,19 @@ if (import.meta.server) {
<p class="text-sm font-medium text-white dark:text-gray-900 truncate"> <p class="text-sm font-medium text-white dark:text-gray-900 truncate">
<UIcon name="i-heroicons-rocket-launch" class="w-5 h-5 align-top flex-shrink-0 pointer-events-none mr-2" /> <UIcon name="i-heroicons-rocket-launch" class="w-5 h-5 align-top flex-shrink-0 pointer-events-none mr-2" />
<span class="font-semibold">Nuxt UI Pro v1.0</span> is out with dashboard components! <span class="font-semibold">Nuxt UI v3-alpha</span> has been released!
</p> </p>
<UButton
to="https://ui3.nuxt.dev"
target="_blank"
label="Try it out"
color="black"
variant="solid"
size="2xs"
trailing-icon="i-heroicons-arrow-right-20-solid"
/>
<div class="flex items-center justify-end lg:flex-1"> <div class="flex items-center justify-end lg:flex-1">
<button <button
class="p-1.5 rounded-md inline-flex hover:bg-primary/90" class="p-1.5 rounded-md inline-flex hover:bg-primary/90"

View File

@@ -48,8 +48,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NavItem } from '@nuxt/content' import type { NavItem } from '@nuxt/content'
import type { HeaderLink } from '#ui-pro/types'
import pkg from '@nuxt/ui-pro/package.json' import pkg from '@nuxt/ui-pro/package.json'
import type { HeaderLink } from '#ui-pro/types'
defineProps<{ defineProps<{
links: HeaderLink[] links: HeaderLink[]

View File

@@ -51,11 +51,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { transformContent } from '@nuxt/content/transformers'
import { upperFirst, camelCase, kebabCase } from 'scule' import { upperFirst, camelCase, kebabCase } from 'scule'
import { useShikiHighlighter } from '~/composables/useShikiHighlighter' import { useShikiHighlighter } from '~/composables/useShikiHighlighter'
// eslint-disable-next-line vue/no-dupe-keys
const props = defineProps({ const props = defineProps({
slug: { slug: {
type: String, type: String,
@@ -90,7 +88,7 @@ const props = defineProps({
default: () => [] default: () => []
}, },
options: { options: {
type: Array as PropType<{ name: string; values: string[]; restriction: 'expected' | 'included' | 'excluded' | 'only' }[]>, type: Array as PropType<{ name: string, values: string[], restriction: 'expected' | 'included' | 'excluded' | 'only' }[]>,
default: () => [] default: () => []
}, },
backgroundClass: { backgroundClass: {
@@ -115,7 +113,6 @@ const props = defineProps({
} }
}) })
// eslint-disable-next-line vue/no-dupe-keys
const baseProps = reactive({ ...props.baseProps }) const baseProps = reactive({ ...props.baseProps })
const componentProps = reactive({ ...props.props }) const componentProps = reactive({ ...props.props })
@@ -159,13 +156,13 @@ const generateOptions = (key: string, schema: { kind: string, schema: [], type:
const schemaOptions = Object.values(schema?.schema || {}) const schemaOptions = Object.values(schema?.schema || {})
if (key.toLowerCase() === 'size' && schemaOptions?.length > 0) { if (key.toLowerCase() === 'size' && schemaOptions?.length > 0) {
const baseSizeOrder = { 'xs': 1, 'sm': 2, 'md': 3, 'lg': 4, 'xl': 5 } const baseSizeOrder = { xs: 1, sm: 2, md: 3, lg: 4, xl: 5 }
schemaOptions.sort((a: string, b: string) => { schemaOptions.sort((a: string, b: string) => {
const aBase = a.match(/[a-zA-Z]+/)[0].toLowerCase() const aBase = a.match(/[a-z]+/i)[0].toLowerCase()
const bBase = b.match(/[a-zA-Z]+/)[0].toLowerCase() const bBase = b.match(/[a-z]+/i)[0].toLowerCase()
const aNum = parseInt(a.match(/\d+/)?.[0]) || 1 const aNum = Number.parseInt(a.match(/\d+/)?.[0]) || 1
const bNum = parseInt(b.match(/\d+/)?.[0]) || 1 const bNum = Number.parseInt(b.match(/\d+/)?.[0]) || 1
if (aBase === bBase) { if (aBase === bBase) {
return aBase === 'xs' ? bNum - aNum : aNum - bNum return aBase === 'xs' ? bNum - aNum : aNum - bNum
@@ -215,7 +212,6 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
} }
}).filter(Boolean)) }).filter(Boolean))
// eslint-disable-next-line vue/no-dupe-keys
const code = computed(() => { const code = computed(() => {
let code = `\`\`\`html let code = `\`\`\`html
<template> <template>
@@ -270,18 +266,20 @@ function renderObject (obj: any) {
return obj return obj
} }
const { data: ast } = await useAsyncData( const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify({ props: componentProps, slots: props.slots, code: props.code })}`, async () => {
`${name}-ast-${JSON.stringify({ props: componentProps, slots: props.slots, code: props.code })}`,
async () => {
let formatted = '' let formatted = ''
try { try {
formatted = await $prettier.format(code.value) || code.value // @ts-ignore
} catch (error) { formatted = await $prettier.format(code.value, {
trailingComma: 'none',
semi: false,
singleQuote: true
})
} catch {
formatted = code.value formatted = code.value
} }
return transformContent('content:_markdown.md', formatted, { return parseMarkdown(formatted, {
markdown: {
highlight: { highlight: {
highlighter, highlighter,
theme: { theme: {
@@ -290,7 +288,6 @@ const { data: ast } = await useAsyncData(
dark: 'material-theme-palenight' dark: 'material-theme-palenight'
} }
} }
}
}) })
}, { watch: [code] }) }, { watch: [code] })
</script> </script>

View File

@@ -1,10 +1,6 @@
<template> <template>
<div class="[&>div>pre]:!rounded-t-none [&>div>pre]:!mt-0"> <div class="[&>div>pre]:!rounded-t-none [&>div>pre]:!mt-0">
<div <div v-if="hasPreview" class="flex border border-gray-200 dark:border-gray-700 relative rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !hasCode, 'border-b-0': hasCode, 'not-prose': !prose }, backgroundClass, extraClass]">
v-if="hasPreview"
class="flex border border-gray-200 dark:border-gray-700 relative rounded-t-md"
:class="[{ 'p-4': padding, 'rounded-b-md': !hasCode, 'border-b-0': hasCode, 'not-prose': !prose }, backgroundClass, extraClass]"
>
<template v-if="component"> <template v-if="component">
<iframe v-if="iframe" :src="`/examples/${component}`" v-bind="iframeProps" :class="backgroundClass" class="w-full" /> <iframe v-if="iframe" :src="`/examples/${component}`" v-bind="iframeProps" :class="backgroundClass" class="w-full" />
<component :is="camelName" v-else v-bind="componentProps" :class="componentClass" /> <component :is="camelName" v-else v-bind="componentProps" :class="componentClass" />
@@ -22,7 +18,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { camelCase } from 'scule' import { camelCase } from 'scule'
import { fetchContentExampleCode } from '~/composables/useContentExamplesCode' import { fetchContentExampleCode } from '~/composables/useContentExamplesCode'
import { transformContent } from '@nuxt/content/transformers'
import { useShikiHighlighter } from '~/composables/useShikiHighlighter' import { useShikiHighlighter } from '~/composables/useShikiHighlighter'
const props = defineProps({ const props = defineProps({
@@ -86,8 +81,7 @@ const highlighter = useShikiHighlighter()
const hasCode = computed(() => !props.hiddenCode && (data?.code || instance.slots.code)) const hasCode = computed(() => !props.hiddenCode && (data?.code || instance.slots.code))
const hasPreview = computed(() => !props.hiddenPreview && (props.component || instance.slots.default)) const hasPreview = computed(() => !props.hiddenPreview && (props.component || instance.slots.default))
const { data: ast } = await useAsyncData(`content-example-${camelName}-ast`, () => transformContent('content:_markdown.md', `\`\`\`vue\n${data?.code ?? ''}\n\`\`\``, { const { data: ast } = await useAsyncData(`content-example-${camelName}-ast`, () => parseMarkdown(`\`\`\`vue\n${data?.code ?? ''}\n\`\`\``, {
markdown: {
highlight: { highlight: {
highlighter, highlighter,
theme: { theme: {
@@ -96,6 +90,5 @@ const { data: ast } = await useAsyncData(`content-example-${camelName}-ast`, ()
dark: 'material-theme-palenight' dark: 'material-theme-palenight'
} }
} }
}
})) }))
</script> </script>

View File

@@ -3,11 +3,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { transformContent } from '@nuxt/content/transformers'
import { upperFirst, camelCase } from 'scule' import { upperFirst, camelCase } from 'scule'
import json5 from 'json5' import json5 from 'json5'
import { useShikiHighlighter } from '~/composables/useShikiHighlighter'
import * as config from '#ui/ui.config' import * as config from '#ui/ui.config'
import { useShikiHighlighter } from '~/composables/useShikiHighlighter'
const props = defineProps({ const props = defineProps({
slug: { slug: {
@@ -18,19 +17,18 @@ const props = defineProps({
const route = useRoute() const route = useRoute()
const highlighter = useShikiHighlighter() const highlighter = useShikiHighlighter()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[route.params.slug.length - 1] const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const camelName = camelCase(slug) const camelName = camelCase(slug)
const name = `U${upperFirst(camelName)}` const name = `U${upperFirst(camelName)}`
const preset = config[camelName] const preset = config[camelName]
const { data: ast } = await useAsyncData(`${name}-preset`, () => transformContent('content:_markdown.md', ` const { data: ast } = await useAsyncData(`${name}-preset`, () => parseMarkdown(`
\`\`\`yml \`\`\`yml
${json5.stringify(preset, null, 2)} ${json5.stringify(preset, null, 2).replace(/,([ |\t\n]+[}|\])])/g, '$1')}
\`\`\`\ \`\`\`\
`, { `, {
markdown: {
highlight: { highlight: {
highlighter, highlighter,
theme: { theme: {
@@ -39,6 +37,5 @@ ${json5.stringify(preset, null, 2)}
dark: 'material-theme-palenight' dark: 'material-theme-palenight'
} }
} }
}
})) }))
</script> </script>

View File

@@ -18,10 +18,12 @@ const actions = [
] ]
const groups = computed(() => const groups = computed(() =>
[commandPaletteRef.value?.query ? { [commandPaletteRef.value?.query
? {
key: 'users', key: 'users',
commands: users commands: users
} : { }
: {
key: 'recent', key: 'recent',
label: 'Recent searches', label: 'Recent searches',
commands: users.slice(0, 1) commands: users.slice(0, 1)

View File

@@ -71,7 +71,7 @@ const ui = {
:autoselect="false" :autoselect="false"
command-attribute="title" command-attribute="title"
:fuse="{ :fuse="{
fuseOptions: { keys: ['title', 'category'] }, fuseOptions: { keys: ['title', 'category'] }
}" }"
placeholder="Search docs" placeholder="Search docs"
/> />

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import { object, string, nonempty, type Infer } from 'superstruct'
import type { FormSubmitEvent } from '#ui/types'
const schema = object({
email: nonempty(string()),
password: nonempty(string())
})
const state = reactive({
email: '',
password: ''
})
type Schema = Infer<typeof schema>
async function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data)
}
</script>
<template>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
<UFormGroup label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormGroup>
<UButton type="submit">
Submit
</UButton>
</UForm>
</template>

View File

@@ -5,15 +5,29 @@ const items = ref(Array(55))
<template> <template>
<UPagination v-model="page" :total="items.length" :ui="{ rounded: 'first-of-type:rounded-s-md last-of-type:rounded-e-md' }"> <UPagination v-model="page" :total="items.length" :ui="{ rounded: 'first-of-type:rounded-s-md last-of-type:rounded-e-md' }">
<template #first="{ onClick }"> <template #first="{ onClick, canGoFirst }">
<UTooltip text="First page"> <UTooltip text="First page">
<UButton icon="i-heroicons-arrow-uturn-left" color="primary" :ui="{ rounded: 'rounded-full' }" class="rtl:[&_span:first-child]:rotate-180 me-2" @click="onClick" /> <UButton
icon="i-heroicons-arrow-uturn-left"
color="primary"
:ui="{ rounded: 'rounded-full' }"
class="rtl:[&_span:first-child]:rotate-180 me-2"
:disabled="!canGoFirst"
@click="onClick"
/>
</UTooltip> </UTooltip>
</template> </template>
<template #last="{ onClick }"> <template #last="{ onClick, canGoLast }">
<UTooltip text="Last page"> <UTooltip text="Last page">
<UButton icon="i-heroicons-arrow-uturn-right-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="rtl:[&_span:last-child]:rotate-180 ms-2" @click="onClick" /> <UButton
icon="i-heroicons-arrow-uturn-right-20-solid"
color="primary"
:ui="{ rounded: 'rounded-full' }"
class="rtl:[&_span:last-child]:rotate-180 ms-2"
:disabled="!canGoLast"
@click="onClick"
/>
</UTooltip> </UTooltip>
</template> </template>
</UPagination> </UPagination>

View File

@@ -5,15 +5,29 @@ const items = ref(Array(55))
<template> <template>
<UPagination v-model="page" :total="items.length" :ui="{ rounded: 'first-of-type:rounded-s-md last-of-type:rounded-e-md' }"> <UPagination v-model="page" :total="items.length" :ui="{ rounded: 'first-of-type:rounded-s-md last-of-type:rounded-e-md' }">
<template #prev="{ onClick }"> <template #prev="{ onClick, canGoPrev }">
<UTooltip text="Previous page"> <UTooltip text="Previous page">
<UButton icon="i-heroicons-arrow-small-left-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="rtl:[&_span:first-child]:rotate-180 me-2" @click="onClick" /> <UButton
icon="i-heroicons-arrow-small-left-20-solid"
color="primary"
:ui="{ rounded: 'rounded-full' }"
class="rtl:[&_span:first-child]:rotate-180 me-2"
:disabled="!canGoPrev"
@click="onClick"
/>
</UTooltip> </UTooltip>
</template> </template>
<template #next="{ onClick }"> <template #next="{ onClick, canGoNext }">
<UTooltip text="Next page"> <UTooltip text="Next page">
<UButton icon="i-heroicons-arrow-small-right-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="rtl:[&_span:last-child]:rotate-180 ms-2" @click="onClick" /> <UButton
icon="i-heroicons-arrow-small-right-20-solid"
color="primary"
:ui="{ rounded: 'rounded-full' }"
class="rtl:[&_span:last-child]:rotate-180 ms-2"
:disabled="!canGoNext"
@click="onClick"
/>
</UTooltip> </UTooltip>
</template> </template>
</UPagination> </UPagination>

View File

@@ -11,7 +11,7 @@ const items = ref(Array(50))
:to="(page: number) => ({ :to="(page: number) => ({
query: { page }, query: { page },
// Hash is specified here to prevent the page from scrolling to the top // Hash is specified here to prevent the page from scrolling to the top
hash: '#links', hash: '#links'
})" })"
/> />
</template> </template>

View File

@@ -38,7 +38,7 @@ const labels = computed({
const showCreateOption = (query, results) => { const showCreateOption = (query, results) => {
const lowercaseQuery = String.prototype.toLowerCase.apply(query || '') const lowercaseQuery = String.prototype.toLowerCase.apply(query || '')
return lowercaseQuery.length >= 3 && !results.find(option => { return lowercaseQuery.length >= 3 && !results.find((option) => {
return String.prototype.toLowerCase.apply(option['name'] || '') === lowercaseQuery return String.prototype.toLowerCase.apply(option['name'] || '') === lowercaseQuery
}) })
} }

View File

@@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
const props = defineProps({ const props = defineProps({
count: { count: {
type: Number, type: Number,
@@ -8,7 +7,7 @@ const props = defineProps({
}) })
const emits = defineEmits<{ const emits = defineEmits<{
close: []; close: []
}>() }>()
</script> </script>

View File

@@ -19,13 +19,13 @@ const columns = [{
}] }]
const selectedColumns = ref(columns) const selectedColumns = ref(columns)
const columnsTable = computed(() => columns.filter((column) => selectedColumns.value.includes(column))) const columnsTable = computed(() => columns.filter(column => selectedColumns.value.includes(column)))
// Selected Rows // Selected Rows
const selectedRows = ref([]) const selectedRows = ref([])
function select(row) { function select(row) {
const index = selectedRows.value.findIndex((item) => item.id === row.id) const index = selectedRows.value.findIndex(item => item.id === row.id)
if (index === -1) { if (index === -1) {
selectedRows.value.push(row) selectedRows.value.push(row)
} else { } else {
@@ -92,10 +92,10 @@ const { data: todos, status } = await useLazyAsyncData<{
}[]>('todos', () => ($fetch as any)(`https://jsonplaceholder.typicode.com/todos${searchStatus.value}`, { }[]>('todos', () => ($fetch as any)(`https://jsonplaceholder.typicode.com/todos${searchStatus.value}`, {
query: { query: {
q: search.value, q: search.value,
'_page': page.value, _page: page.value,
'_limit': pageCount.value, _limit: pageCount.value,
'_sort': sort.value.column, _sort: sort.value.column,
'_order': sort.value.direction _order: sort.value.direction
} }
}), { }), {
default: () => [], default: () => [],

View File

@@ -32,7 +32,7 @@ const people = [{
}] }]
function select(row) { function select(row) {
const index = selected.value.findIndex((item) => item.id === row.id) const index = selected.value.findIndex(item => item.id === row.id)
if (index === -1) { if (index === -1) {
selected.value.push(row) selected.value.push(row)
} else { } else {

View File

@@ -0,0 +1,75 @@
<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',
disabledExpand: true
}, {
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',
disabledExpand: true
}, {
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',
disabledExpand: true
}]
const columns = [
{
label: 'Name',
key: 'name'
},
{
label: 'title',
key: 'title'
},
{
label: 'Email',
key: 'email'
},
{
label: 'role',
key: 'role'
}
]
const expand = ref({
openedRows: [],
row: null
})
</script>
<template>
<UTable v-model:expand="expand" :rows="people" :columns="columns">
<template #expand="{ row }">
<div class="p-4">
<pre>{{ row }}</pre>
</div>
</template>
</UTable>
</template>

View File

@@ -0,0 +1,65 @@
<script setup lang='ts'>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member',
hasExpand: false
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin',
hasExpand: true
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member',
hasExpand: false
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin',
hasExpand: true
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner',
hasExpand: false
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member',
hasExpand: true
}]
const expand = ref({
openedRows: [people.find(v => v.hasExpand)],
row: {}
})
</script>
<template>
<UTable v-model:expand="expand" :rows="people">
<template #expand="{ row }">
<div class="p-4">
<pre>{{ row }}</pre>
</div>
</template>
<template #expand-action="{ row, isExpanded, toggle }">
<UButton v-if="row.hasExpand" @click="toggle">
{{ isExpanded ? 'collapse' : 'expand' }}
</UButton>
</template>
</UTable>
</template>

View File

@@ -1,4 +1,4 @@
<script setup> <script setup lang='ts'>
const people = [{ const people = [{
id: 1, id: 1,
name: 'Lindsay Walton', name: 'Lindsay Walton',
@@ -36,10 +36,15 @@ const people = [{
email: 'floyd.miles@example.com', email: 'floyd.miles@example.com',
role: 'Member' role: 'Member'
}] }]
const expand = ref({
openedRows: [people[0]],
row: {}
})
</script> </script>
<template> <template>
<UTable :rows="people"> <UTable v-model:expand="expand" :rows="people">
<template #expand="{ row }"> <template #expand="{ row }">
<div class="p-4"> <div class="p-4">
<pre>{{ row }}</pre> <pre>{{ row }}</pre>

View File

@@ -67,7 +67,9 @@ const pending = ref(true)
} }
@keyframes loader-6 { @keyframes loader-6 {
0%, 100% {
0%,
100% {
transform: none; transform: none;
} }

View File

@@ -53,7 +53,7 @@ const people = [{
role: 'Member' role: 'Member'
}] }]
const items = (row) => [ const items = row => [
[{ [{
label: 'Edit', label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid', icon: 'i-heroicons-pencil-square-20-solid',

View File

@@ -18,7 +18,7 @@ const router = useRouter()
const selected = computed({ const selected = computed({
get() { get() {
const index = items.findIndex((item) => item.label === route.query.tab) const index = items.findIndex(item => item.label === route.query.tab)
if (index === -1) { if (index === -1) {
return 0 return 0
} }

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { useBreakpoints, breakpointsTailwind } from '@vueuse/core'
import { DatePicker as VCalendarDatePicker } from 'v-calendar' import { DatePicker as VCalendarDatePicker } from 'v-calendar'
// @ts-ignore // @ts-ignore
import type { DatePickerDate, DatePickerRangeObject } from 'v-calendar/dist/types/src/use/datePicker' import type { DatePickerDate, DatePickerRangeObject } from 'v-calendar/dist/types/src/use/datePicker'
@@ -26,22 +25,27 @@ const date = computed({
} }
}) })
const breakpoints = useBreakpoints(breakpointsTailwind)
const smallerThanSm = breakpoints.smaller('sm')
const attrs = { const attrs = {
transparent: true, 'transparent': true,
borderless: true, 'borderless': true,
color: 'primary', 'color': 'primary',
'is-dark': { selector: 'html', darkClass: 'dark' }, 'is-dark': { selector: 'html', darkClass: 'dark' },
'first-day-of-week': 2 'first-day-of-week': 2
} }
</script> </script>
<template> <template>
<VCalendarDatePicker v-if="date && (date as DatePickerRangeObject)?.start && (date as DatePickerRangeObject)?.end" v-model.range="date" :columns="smallerThanSm ? 1 : 2" :rows="smallerThanSm ? 2 : 1" v-bind="{ ...attrs, ...$attrs }" /> <VCalendarDatePicker
<VCalendarDatePicker v-else v-model="date" v-bind="{ ...attrs, ...$attrs }" /> v-if="date && (date as DatePickerRangeObject)?.start && (date as DatePickerRangeObject)?.end"
v-model.range="date"
:columns="2"
v-bind="{ ...attrs, ...$attrs }"
/>
<VCalendarDatePicker
v-else
v-model="date"
v-bind="{ ...attrs, ...$attrs }"
/>
</template> </template>
<style> <style>

View File

@@ -29,7 +29,7 @@
v-else v-else
class="font-semibold flex flex-col gap-1 text-center" class="font-semibold flex flex-col gap-1 text-center"
:class="[ :class="[
!block.slot && (block.inactive || block.inactive === undefined ? 'text-gray-900 dark:text-white' : 'text-white dark:text-gray-900'), !block.slot && (block.inactive || block.inactive === undefined ? 'text-gray-900 dark:text-white' : 'text-white dark:text-gray-900')
]" ]"
> >
{{ block.name }} {{ block.name }}

View File

@@ -40,6 +40,7 @@ function createGrid () {
grid.value = [] grid.value = []
for (let i = 0; i <= rows.value; i++) { for (let i = 0; i <= rows.value; i++) {
// eslint-disable-next-line unicorn/no-new-array
grid.value.push(new Array(cols.value).fill(null)) grid.value.push(new Array(cols.value).fill(null))
} }
} }

View File

@@ -7,7 +7,9 @@ export async function fetchComponentMeta (name: string) {
await state.value[name] await state.value[name]
return state.value[name] return state.value[name]
} }
if (state.value[name]) { return state.value[name] } if (state.value[name]) {
return state.value[name]
}
// Store promise to avoid multiple calls // Store promise to avoid multiple calls

View File

@@ -8,7 +8,9 @@ export async function fetchContentExampleCode (name?: string) {
await state.value[name] await state.value[name]
return state.value[name] return state.value[name]
} }
if (state.value[name]) { return state.value[name] } if (state.value[name]) {
return state.value[name]
}
// add to nitro prerender // add to nitro prerender
if (import.meta.server) { if (import.meta.server) {

View File

@@ -38,9 +38,14 @@ The following example is styled based on the `primary` and `gray` colors and sup
```vue [components/DatePicker.vue] ```vue [components/DatePicker.vue]
<script setup lang="ts"> <script setup lang="ts">
import { DatePicker as VCalendarDatePicker } from 'v-calendar' import { DatePicker as VCalendarDatePicker } from 'v-calendar'
// @ts-ignore
import type { DatePickerDate, DatePickerRangeObject } from 'v-calendar/dist/types/src/use/datePicker' import type { DatePickerDate, DatePickerRangeObject } from 'v-calendar/dist/types/src/use/datePicker'
import 'v-calendar/dist/style.css' import 'v-calendar/dist/style.css'
defineOptions({
inheritAttrs: false
})
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: [Date, Object] as PropType<DatePickerDate | DatePickerRangeObject | null>, type: [Date, Object] as PropType<DatePickerDate | DatePickerRangeObject | null>,
@@ -59,17 +64,26 @@ const date = computed({
}) })
const attrs = { const attrs = {
transparent: true, 'transparent': true,
borderless: true, 'borderless': true,
color: 'primary', 'color': 'primary',
'is-dark': { selector: 'html', darkClass: 'dark' }, 'is-dark': { selector: 'html', darkClass: 'dark' },
'first-day-of-week': 2, 'first-day-of-week': 2
} }
</script> </script>
<template> <template>
<VCalendarDatePicker v-if="date && (typeof date === 'object')" v-model.range="date" :columns="2" v-bind="{ ...attrs, ...$attrs }" /> <VCalendarDatePicker
<VCalendarDatePicker v-else v-model="date" v-bind="{ ...attrs, ...$attrs }" /> v-if="date && (date as DatePickerRangeObject)?.start && (date as DatePickerRangeObject)?.end"
v-model.range="date"
:columns="2"
v-bind="{ ...attrs, ...$attrs }"
/>
<VCalendarDatePicker
v-else
v-model="date"
v-bind="{ ...attrs, ...$attrs }"
/>
</template> </template>
<style> <style>

View File

@@ -8,13 +8,13 @@ links:
## Usage ## 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://github.com/fabian-hiller/valibot), 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), [Superstruct](https://github.com/ianstormtaylor/superstruct), or your own validation logic.
It works with the [FormGroup](/components/form-group) 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: The form component requires two props:
- `state` - a reactive object holding the form's state. - `state` - a reactive object holding the form's state.
- `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). - `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), [Valibot](https://github.com/fabian-hiller/valibot) or [Superstruct](https://github.com/ianstormtaylor/superstruct).
::callout{icon="i-heroicons-light-bulb"} ::callout{icon="i-heroicons-light-bulb"}
Note that **no validation library is included** by default, so ensure you **install the one you need**. Note that **no validation library is included** by default, so ensure you **install the one you need**.
@@ -52,6 +52,13 @@ Note that **no validation library is included** by default, so ensure you **inst
class: 'w-60' class: 'w-60'
--- ---
:: ::
::component-example{label="Superstruct"}
---
component: 'form-example-superstruct'
componentProps:
class: 'w-60'
---
::
:: ::
## Custom validation ## Custom validation

View File

@@ -32,7 +32,7 @@ This component does not support multiple values. Use the [SelectMenu](/component
### Objects ### Objects
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`. You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`. Additionally, you can use dot notation (e.g., `user.name`) to access nested object properties.
::component-example ::component-example
--- ---
@@ -174,6 +174,8 @@ componentProps:
Use the `#option-empty` slot to customize the content displayed when the `searchable` prop is `true` and there is no options. You will have access to the `query` property in the slot scope. Use the `#option-empty` slot to customize the content displayed when the `searchable` prop is `true` and there is no options. You will have access to the `query` property in the slot scope.
You can also configure this globally through the `ui.inputMenu.default.optionEmpty.label` config. The token `{query}` will be replaced by `query` property. Defaults to `No results for "{query}".`.
::component-example ::component-example
--- ---
component: 'input-menu-example-option-empty-slot' component: 'input-menu-example-option-empty-slot'
@@ -186,6 +188,8 @@ componentProps:
Use the `#empty` slot to customize the content displayed when there is no options. Defaults to `No options.`. Use the `#empty` slot to customize the content displayed when there is no options. Defaults to `No options.`.
You can also configure this globally through the `ui.inputMenu.default.empty.label` config. Defaults to `No options.`.
::component-example ::component-example
--- ---
component: 'input-menu-example-empty-slot' component: 'input-menu-example-empty-slot'

View File

@@ -29,12 +29,16 @@ export default defineAppConfig({
ui: { ui: {
notifications: { notifications: {
// Show toasts at the top right of the screen // Show toasts at the top right of the screen
position: 'top-0 right-0' position: 'top-0 bottom-[unset]'
} }
} }
}) })
``` ```
::callout{icon="i-heroicons-light-bulb"}
The `position` defaults to `bottom-0 end-0`, the `bottom-[unset]` class overrides `bottom-0` so the result is `top-0 end-0`.
::
Then, you can use the `useToast` composable to add notifications to your app: Then, you can use the `useToast` composable to add notifications to your app:
:component-example{component="notification-example-basic"} :component-example{component="notification-example-basic"}

View File

@@ -40,7 +40,7 @@ componentProps:
### Objects ### Objects
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`. You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`. Additionally, you can use dot notation (e.g., `user.name`) to access nested object properties.
::component-example ::component-example
--- ---
@@ -85,7 +85,7 @@ Learn how to customize icons from the [Select](/components/select#icon) componen
Use the `searchable` prop to enable search. Use the `searchable` prop to enable search.
Use the `searchable-placeholder` prop to set a different placeholder. Use the `searchable-placeholder` prop to set a different placeholder or globally through the `ui.selectMenu.default.searchablePlaceholder.label` config. Defaults to `Search...`.
This will use Headless UI [Combobox](https://headlessui.com/v1/vue/combobox) component instead of [Listbox](https://headlessui.com/v1/vue/listbox). This will use Headless UI [Combobox](https://headlessui.com/v1/vue/combobox) component instead of [Listbox](https://headlessui.com/v1/vue/listbox).
@@ -258,6 +258,8 @@ componentProps:
Use the `#option-empty` slot to customize the content displayed when the `searchable` prop is `true` and there is no options. You will have access to the `query` property in the slot scope. Use the `#option-empty` slot to customize the content displayed when the `searchable` prop is `true` and there is no options. You will have access to the `query` property in the slot scope.
You can also configure this globally through the `ui.selectMenu.default.optionEmpty.label` config. The token `{query}` will be replaced by `query` property. Defaults to `No results for "{query}".`.
::component-example ::component-example
--- ---
component: 'select-menu-example-option-empty-slot' component: 'select-menu-example-option-empty-slot'
@@ -276,7 +278,9 @@ An example is available in the [Creatable](#creatable) section.
### `empty` ### `empty`
Use the `#empty` slot to customize the content displayed when there is no options. Defaults to `No options.`. Use the `#empty` slot to customize the content displayed when there is no options.
You can also configure this globally through the `ui.selectMenu.default.empty.label` config. Defaults to `No options.`.
::component-example ::component-example
--- ---

View File

@@ -315,10 +315,13 @@ componentProps:
### Expandable :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"} ### 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. You can use the `v-model:expand` to enables row expansion functionality in the table component. It maintains an object containing an `openedRows` an array and `row` an object, which tracks the indices of currently expanded rows.
When using the expand slot, you have access to the `row` property in the slot scope, which contains the data of the row that triggered the expand/collapse action. This allows you to customize the expanded content based on the row's data.
::component-example{class="grid"} ::component-example{class="grid"}
--- ---
extraClass: 'overflow-hidden'
padding: false padding: false
component: 'table-example-expandable' component: 'table-example-expandable'
componentProps: componentProps:
@@ -326,6 +329,73 @@ componentProps:
--- ---
:: ::
#### Event expand
The `@update:expand` event is emitted when a row is expanded. This event provides the current state of expanded rows and the data of the row that triggered the event.
To use the `@update:expand` event, add it to your `UTable` component. The event handler will receive an object with the following properties:
- `openedRows`: An array of indices of the currently expanded rows.
- `row`: The row data that triggered the expand/collapse action.
```vue
<script setup lang="ts">
const { data, pending } = await useLazyFetch(() => `/api/users`)
const handleExpand = ({ openedRows, row }) => {
console.log('opened Rows:', openedRows);
console.log('Row Data:', row);
};
const expand = ref({
openedRows: [],
row: null
})
</script>
<template>
<UTable v-model="expand" :loading="pending" :rows="data" @update:expand="handleExpand">
<template #expand="{ row }">
<div class="p-4">
<pre>{{ row }}</pre>
</div>
</template>
</UTable>
</template>
```
#### Multiple expand
Controls whether multiple rows can be expanded simultaneously in the table.
```vue
<template>
<!-- Allow only one row to be expanded at a time -->
<UTable :multiple-expand="false" />
<!-- Default behavior: Allow multiple rows to be expanded simultaneously -->
<UTable :multiple-expand="true" />
<!-- Or simply -->
<UTable />
</template>
```
#### Disable Row Expansion
You can disable the expansion functionality for specific rows in the UTable component by adding the `disabledExpand` property to your row data.
> Important: When using `disabledExpand`, you must define the `columns` prop for the UTable component. Otherwise, the table will render all properties as columns, including the `disabledExpand` property.
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-disabled-expandable'
componentProps:
class: 'flex-1'
---
::
### Loading ### Loading
Use the `loading` prop to indicate that data is currently loading with an indeterminate [Progress](/components/progress#indeterminate) bar. Use the `loading` prop to indicate that data is currently loading with an indeterminate [Progress](/components/progress#indeterminate) bar.
@@ -448,6 +518,43 @@ componentProps:
--- ---
:: ::
### `expand-action`
The `#expand-action` slot allows you to customize the expansion control interface for expandable table rows. This feature provides a flexible way to implement custom expand/collapse functionality while maintaining access to essential row data and state.
#### Usage
```vue
<template>
<UTable>
<template #expand-action="{ row, toggle, isExpanded }">
<!-- Your custom expand action content -->
</template>
</UTable>
</template>
```
#### Slot Props
The slot provides three key props:
| Prop | Type | Description |
|------|------|-------------|
| `row` | `Object` | Contains the current row's data |
| `toggle` | `Function` | Function to toggle the expanded state |
| `isExpanded` | `Boolean` | Current expansion state of the row |
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-expand-action-slot'
componentProps:
class: 'flex-1'
---
::
### `loading-state` ### `loading-state`
Use the `#loading-state` slot to customize the loading state. Use the `#loading-state` slot to customize the loading state.

View File

@@ -29,8 +29,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { NuxtError } from '#app'
import type { ParsedContent } from '@nuxt/content' import type { ParsedContent } from '@nuxt/content'
import type { NuxtError } from '#app'
useSeoMeta({ useSeoMeta({
title: 'Page not found', title: 'Page not found',
@@ -57,7 +57,8 @@ const links = computed(() => {
icon: 'i-heroicons-book-open', icon: 'i-heroicons-book-open',
to: '/getting-started', to: '/getting-started',
active: route.path.startsWith('/getting-started') || route.path.startsWith('/components') active: route.path.startsWith('/getting-started') || route.path.startsWith('/components')
}, ...(navigation.value.find(item => item._path === '/pro') ? [{ }, ...(navigation.value.find(item => item._path === '/pro')
? [{
label: 'Pro', label: 'Pro',
icon: 'i-heroicons-square-3-stack-3d', icon: 'i-heroicons-square-3-stack-3d',
to: '/pro', to: '/pro',
@@ -70,7 +71,8 @@ const links = computed(() => {
label: 'Templates', label: 'Templates',
icon: 'i-heroicons-computer-desktop', icon: 'i-heroicons-computer-desktop',
to: '/pro/templates' to: '/pro/templates'
}] : []), { }]
: []), {
label: 'Releases', label: 'Releases',
icon: 'i-heroicons-rocket-launch', icon: 'i-heroicons-rocket-launch',
to: '/releases' to: '/releases'

View File

@@ -1,3 +1,6 @@
import { existsSync, readFileSync } from 'node:fs'
import fsp from 'node:fs/promises'
import { dirname, join } from 'pathe'
import { import {
defineNuxtModule, defineNuxtModule,
addTemplate, addTemplate,
@@ -5,10 +8,6 @@ import {
createResolver createResolver
} from '@nuxt/kit' } from '@nuxt/kit'
import { existsSync, readFileSync } from 'fs'
import { dirname, join } from 'pathe'
import fsp from 'fs/promises'
export default defineNuxtModule({ export default defineNuxtModule({
meta: { meta: {
name: 'content-examples-code' name: 'content-examples-code'
@@ -74,7 +73,7 @@ export default defineNuxtModule({
nuxt.hook('components:extend', async (_components) => { nuxt.hook('components:extend', async (_components) => {
components = _components components = _components
.filter((v) => v.shortPath.includes('components/content/examples/')) .filter(v => v.shortPath.includes('components/content/examples/'))
.reduce((acc, component) => { .reduce((acc, component) => {
acc[component.pascalName] = component acc[component.pascalName] = component
return acc return acc

View File

@@ -8,10 +8,12 @@ const { resolve } = createResolver(import.meta.url)
export default defineNuxtConfig({ export default defineNuxtConfig({
// @ts-ignore // @ts-ignore
extends: process.env.NUXT_UI_PRO_PATH ? [ extends: process.env.NUXT_UI_PRO_PATH
? [
process.env.NUXT_UI_PRO_PATH, process.env.NUXT_UI_PRO_PATH,
resolve(process.env.NUXT_UI_PRO_PATH, '.docs') resolve(process.env.NUXT_UI_PRO_PATH, '.docs')
] : [ ]
: [
'@nuxt/ui-pro', '@nuxt/ui-pro',
process.env.NUXT_GITHUB_TOKEN && ['github:nuxt/ui-pro/.docs#dev', { giget: { auth: process.env.NUXT_GITHUB_TOKEN } }] process.env.NUXT_GITHUB_TOKEN && ['github:nuxt/ui-pro/.docs#dev', { giget: { auth: process.env.NUXT_GITHUB_TOKEN } }]
].filter(Boolean), ].filter(Boolean),
@@ -29,15 +31,8 @@ export default defineNuxtConfig({
'modules/content-examples-code' 'modules/content-examples-code'
], ],
runtimeConfig: { site: {
public: { url: 'https://ui.nuxt.com'
version: pkg.version
}
},
ui: {
global: true,
safelistColors: excludeColors(colors)
}, },
content: { content: {
@@ -48,31 +43,42 @@ export default defineNuxtConfig({
] ]
}, },
sources: { sources: {
pro: process.env.NUXT_UI_PRO_PATH ? { pro: process.env.NUXT_UI_PRO_PATH
? {
prefix: '/pro', prefix: '/pro',
driver: 'fs', driver: 'fs',
base: resolve(process.env.NUXT_UI_PRO_PATH, '.docs/content/pro') base: resolve(process.env.NUXT_UI_PRO_PATH, '.docs/content/pro')
} : process.env.NUXT_GITHUB_TOKEN ? { }
: process.env.NUXT_GITHUB_TOKEN
? {
prefix: '/pro', prefix: '/pro',
driver: 'github', driver: 'github',
repo: 'nuxt/ui-pro', repo: 'nuxt/ui-pro',
branch: 'dev', branch: 'dev',
dir: '.docs/content/pro', dir: '.docs/content/pro',
token: process.env.NUXT_GITHUB_TOKEN || '' token: process.env.NUXT_GITHUB_TOKEN || ''
} : undefined }
: undefined
} }
}, },
image: { ui: {
provider: 'ipx' global: true,
safelistColors: excludeColors(colors)
}, },
icon: { runtimeConfig: {
clientBundle: { public: {
scan: true version: pkg.version
} }
}, },
routeRules: {
'/components': { redirect: '/components/accordion', prerender: false }
},
compatibilityDate: '2024-07-23',
nitro: { nitro: {
prerender: { prerender: {
routes: [ routes: [
@@ -86,8 +92,32 @@ export default defineNuxtConfig({
} }
}, },
routeRules: { vite: {
'/components': { redirect: '/components/accordion', prerender: false } optimizeDeps: {
include: ['date-fns']
}
},
typescript: {
strict: false
},
hooks: {
// Related to https://github.com/nuxt/nuxt/pull/22558
'components:extend': (components) => {
components.forEach((component) => {
if (component.shortPath.includes(process.env.NUXT_UI_PRO_PATH || '@nuxt/ui-pro')) {
component.global = true
} else if (component.global) {
component.global = 'sync'
}
})
}
},
cloudflareAnalytics: {
token: '1e2b0c5e9a214f0390b9b94e043d8d4c',
scriptPath: false
}, },
componentMeta: { componentMeta: {
@@ -111,37 +141,13 @@ export default defineNuxtConfig({
} }
}, },
cloudflareAnalytics: { icon: {
token: '1e2b0c5e9a214f0390b9b94e043d8d4c', clientBundle: {
scriptPath: false scan: true
}
}, },
hooks: { image: {
// Related to https://github.com/nuxt/nuxt/pull/22558 provider: 'ipx'
'components:extend': (components) => {
components.forEach((component) => {
if (component.shortPath.includes(process.env.NUXT_UI_PRO_PATH || '@nuxt/ui-pro')) {
component.global = true
} else if (component.global) {
component.global = 'sync'
} }
}) })
}
},
typescript: {
strict: false
},
site: {
url: 'https://ui.nuxt.com'
},
vite: {
optimizeDeps: {
include: ['date-fns']
}
},
compatibilityDate: '2024-07-23'
})

View File

@@ -4,24 +4,22 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@iconify-json/heroicons": "^1.2.1", "@iconify-json/heroicons": "^1.2.1",
"@iconify-json/simple-icons": "^1.2.7", "@iconify-json/simple-icons": "^1.2.11",
"@iconify-json/vscode-icons": "^1.2.2", "@iconify-json/vscode-icons": "^1.2.2",
"@nuxt/content": "^2.13.2", "@nuxt/content": "^2.13.4",
"@nuxt/eslint-config": "^0.4.0", "@nuxt/fonts": "^0.10.2",
"@nuxt/fonts": "^0.10.0",
"@nuxt/image": "^1.8.1", "@nuxt/image": "^1.8.1",
"@nuxt/ui": "latest", "@nuxt/ui": "latest",
"@nuxt/ui-pro": "^1.4.3", "@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@1.4.4-28846941.4241122",
"@nuxtjs/plausible": "^1.0.3", "@nuxtjs/plausible": "^1.0.3",
"@octokit/rest": "^21.0.2", "@octokit/rest": "^21.0.2",
"@vueuse/nuxt": "^11.1.0", "@vueuse/nuxt": "^11.2.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"eslint": "^8.57.0",
"joi": "^17.13.3", "joi": "^17.13.3",
"nuxt": "^3.13.2", "nuxt": "^3.14.0",
"nuxt-cloudflare-analytics": "^1.0.8", "nuxt-cloudflare-analytics": "^1.0.8",
"nuxt-component-meta": "^0.8.2", "nuxt-component-meta": "^0.9.0",
"nuxt-og-image": "^3.0.4", "nuxt-og-image": "^3.0.8",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"v-calendar": "^3.1.2", "v-calendar": "^3.1.2",

View File

@@ -295,7 +295,7 @@
wrapper: 'px-4 py-2.5 border-gray-800/10 dark:border-gray-200/10 cursor-pointer', wrapper: 'px-4 py-2.5 border-gray-800/10 dark:border-gray-200/10 cursor-pointer',
icon: { icon: {
wrapper: 'mb-2 p-1', wrapper: 'mb-2 p-1',
base: 'h-4 w-4', base: 'h-4 w-4'
}, },
title: 'text-sm', title: 'text-sm',
description: 'text-xs' description: 'text-xs'
@@ -466,7 +466,8 @@ const steps = {
const inc = computed(() => (height.value - 32 - 64 - 32 - 32) / 4) const inc = computed(() => (height.value - 32 - 64 - 32 - 32) / 4)
const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(steps.docs) ? [{ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(steps.docs)
? [{
class: 'inset-x-0 top-20 bottom-20 overflow-hidden', class: 'inset-x-0 top-20 bottom-20 overflow-hidden',
inactive: true, inactive: true,
children: [{ children: [{
@@ -506,34 +507,42 @@ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(
to: '/pro/components/landing-grid', to: '/pro/components/landing-grid',
class: ['inset-x-4 bottom-4 top-48', isAfterStep(steps.landing + 8) && 'grid grid-cols-4 gap-4 p-4'].filter(Boolean).join(' '), class: ['inset-x-4 bottom-4 top-48', isAfterStep(steps.landing + 8) && 'grid grid-cols-4 gap-4 p-4'].filter(Boolean).join(' '),
inactive: isAfterStep(steps.landing + 8), inactive: isAfterStep(steps.landing + 8),
children: [isAfterStep(steps.landing + 9) ? { children: [isAfterStep(steps.landing + 9)
? {
slot: 'landing-card-1', slot: 'landing-card-1',
class: '!relative' class: '!relative'
} : { }
: {
name: 'ULandingCard', name: 'ULandingCard',
to: '/pro/components/landing-card', to: '/pro/components/landing-card',
class: '!relative h-full', class: '!relative h-full',
inactive: false inactive: false
}, isAfterStep(steps.landing + 9) ? { }, isAfterStep(steps.landing + 9)
? {
slot: 'landing-card-2', slot: 'landing-card-2',
class: '!relative h-full' class: '!relative h-full'
} : { }
: {
name: 'ULandingCard', name: 'ULandingCard',
to: '/pro/components/landing-card', to: '/pro/components/landing-card',
class: '!relative h-full', class: '!relative h-full',
inactive: false inactive: false
}, isAfterStep(steps.landing + 9) ? { }, isAfterStep(steps.landing + 9)
? {
slot: 'landing-card-3', slot: 'landing-card-3',
class: '!relative h-full' class: '!relative h-full'
} : { }
: {
name: 'ULandingCard', name: 'ULandingCard',
to: '/pro/components/landing-card', to: '/pro/components/landing-card',
class: '!relative h-full', class: '!relative h-full',
inactive: false inactive: false
}, isAfterStep(steps.landing + 9) ? { }, isAfterStep(steps.landing + 9)
? {
slot: 'landing-card-4', slot: 'landing-card-4',
class: '!relative h-full' class: '!relative h-full'
} : { }
: {
name: 'ULandingCard', name: 'ULandingCard',
to: '/pro/components/landing-card', to: '/pro/components/landing-card',
class: '!relative h-full', class: '!relative h-full',
@@ -563,25 +572,30 @@ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(
}] }]
}] }]
}].filter(Boolean) }].filter(Boolean)
}] : []) }]
: [])
const docsBlocks = computed(() => [isAfterStep(steps.docs) && { const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
name: 'UPage', name: 'UPage',
to: '/pro/components/page', to: '/pro/components/page',
class: 'inset-x-0 top-20 bottom-20', class: 'inset-x-0 top-20 bottom-20',
inactive: isAfterStep(steps.docs + 1), inactive: isAfterStep(steps.docs + 1),
children: [isAfterStep(steps.docs + 2) ? { children: [isAfterStep(steps.docs + 2)
? {
name: 'UAside', name: 'UAside',
to: '/pro/components/aside', to: '/pro/components/aside',
class: 'left-4 inset-y-4 w-64', class: 'left-4 inset-y-4 w-64',
inactive: isAfterStep(steps.docs + 3), inactive: isAfterStep(steps.docs + 3),
children: [isAfterStep(steps.docs + 4) ? { children: [isAfterStep(steps.docs + 4)
? {
slot: 'aside-top', slot: 'aside-top',
class: 'inset-x-4 top-4' class: 'inset-x-4 top-4'
} : { }
: {
name: '#top', name: '#top',
class: 'inset-x-4 top-4 h-9' class: 'inset-x-4 top-4 h-9'
}, isAfterStep(steps.docs + 5) ? { }, isAfterStep(steps.docs + 5)
? {
name: 'UNavigationTree', name: 'UNavigationTree',
to: '/pro/components/navigation-tree', to: '/pro/components/navigation-tree',
class: ['inset-x-4 top-[4.25rem] bottom-4', isAfterStep(steps.docs + 6) && '!bg-transparent !border-0'].join(' '), class: ['inset-x-4 top-[4.25rem] bottom-4', isAfterStep(steps.docs + 6) && '!bg-transparent !border-0'].join(' '),
@@ -590,19 +604,23 @@ const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
slot: 'aside-default', slot: 'aside-default',
class: 'inset-0' class: 'inset-0'
}] }]
} : { }
: {
name: '#default', name: '#default',
class: 'inset-x-4 top-[4.25rem] bottom-4' class: 'inset-x-4 top-[4.25rem] bottom-4'
}] }]
} : { }
: {
name: '#left', name: '#left',
class: 'left-4 inset-y-4 w-64' class: 'left-4 inset-y-4 w-64'
}, isAfterStep(steps.docs + 7) ? { }, isAfterStep(steps.docs + 7)
? {
name: 'UPage', name: 'UPage',
to: '/pro/components/page', to: '/pro/components/page',
class: 'left-72 right-4 inset-y-4', class: 'left-72 right-4 inset-y-4',
inactive: isAfterStep(steps.docs + 8), inactive: isAfterStep(steps.docs + 8),
children: [...(isAfterStep(steps.docs + 9) ? [{ children: [...(isAfterStep(steps.docs + 9)
? [{
name: 'UPageHeader', name: 'UPageHeader',
to: '/pro/components/page-header', to: '/pro/components/page-header',
class: 'top-4 left-4 right-72 h-32', class: 'top-4 left-4 right-72 h-32',
@@ -619,19 +637,23 @@ const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
children: [{ children: [{
slot: 'page-body', slot: 'page-body',
class: 'inset-x-4 top-4 justify-start' class: 'inset-x-4 top-4 justify-start'
}, isAfterStep(steps.docs + 12) ? { }, isAfterStep(steps.docs + 12)
? {
slot: 'content-surround', slot: 'content-surround',
class: 'bottom-4 inset-x-4 h-28' class: 'bottom-4 inset-x-4 h-28'
} : { }
: {
name: 'UContentSurround', name: 'UContentSurround',
to: '/pro/components/content-surround', to: '/pro/components/content-surround',
class: 'bottom-4 inset-x-4 h-28', class: 'bottom-4 inset-x-4 h-28',
inactive: false inactive: false
}] }]
}] : [{ }]
: [{
name: '#default', name: '#default',
class: 'left-4 right-72 inset-y-4' class: 'left-4 right-72 inset-y-4'
}]), isAfterStep(steps.docs + 13) ? { }]), isAfterStep(steps.docs + 13)
? {
name: 'UContentToc', name: 'UContentToc',
to: '/pro/components/content-toc', to: '/pro/components/content-toc',
class: 'right-4 inset-y-4 w-64', class: 'right-4 inset-y-4 w-64',
@@ -640,11 +662,13 @@ const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
slot: 'content-toc', slot: 'content-toc',
class: 'inset-4 overflow-y-auto' class: 'inset-4 overflow-y-auto'
}] }]
} : { }
: {
name: '#right', name: '#right',
class: 'right-4 inset-y-4 w-64' class: 'right-4 inset-y-4 w-64'
}] }]
} : { }
: {
name: '#default', name: '#default',
class: 'left-72 right-4 inset-y-4' class: 'left-72 right-4 inset-y-4'
}] }]
@@ -655,22 +679,28 @@ const blocks = computed(() => [isAfterStep(steps.header) && {
to: '/pro/components/header', to: '/pro/components/header',
class: 'h-16 inset-x-0 top-0', class: 'h-16 inset-x-0 top-0',
inactive: isAfterStep(steps.header + 1), inactive: isAfterStep(steps.header + 1),
children: [isAfterStep(steps.header + 2) ? { children: [isAfterStep(steps.header + 2)
? {
slot: 'header-left', slot: 'header-left',
class: 'left-4 top-4' class: 'left-4 top-4'
} : { }
: {
name: '#left', name: '#left',
class: 'left-4 inset-y-4 w-64' class: 'left-4 inset-y-4 w-64'
}, isAfterStep(steps.header + 3) ? { }, isAfterStep(steps.header + 3)
? {
slot: 'header-center', slot: 'header-center',
class: 'inset-x-72 top-5' class: 'inset-x-72 top-5'
} : { }
: {
name: '#center', name: '#center',
class: 'inset-x-72 inset-y-4' class: 'inset-x-72 inset-y-4'
}, isAfterStep(steps.header + 4) ? { }, isAfterStep(steps.header + 4)
? {
slot: 'header-right', slot: 'header-right',
class: 'right-4 top-4' class: 'right-4 top-4'
} : { }
: {
name: '#right', name: '#right',
class: 'right-4 inset-y-4 w-64' class: 'right-4 inset-y-4 w-64'
}] }]
@@ -679,22 +709,28 @@ const blocks = computed(() => [isAfterStep(steps.header) && {
to: '/pro/components/footer', to: '/pro/components/footer',
class: 'h-16 inset-x-0 bottom-0', class: 'h-16 inset-x-0 bottom-0',
inactive: isAfterStep(steps.footer + 1), inactive: isAfterStep(steps.footer + 1),
children: [isAfterStep(steps.footer + 2) ? { children: [isAfterStep(steps.footer + 2)
? {
slot: 'footer-left', slot: 'footer-left',
class: 'left-4 bottom-5' class: 'left-4 bottom-5'
} : { }
: {
name: '#left', name: '#left',
class: 'left-4 inset-y-4 w-64' class: 'left-4 inset-y-4 w-64'
}, isAfterStep(steps.footer + 3) ? { }, isAfterStep(steps.footer + 3)
? {
slot: 'footer-center', slot: 'footer-center',
class: 'inset-x-72 bottom-5' class: 'inset-x-72 bottom-5'
} : { }
: {
name: '#center', name: '#center',
class: 'inset-x-72 inset-y-4' class: 'inset-x-72 inset-y-4'
}, isAfterStep(steps.footer + 4) ? { }, isAfterStep(steps.footer + 4)
? {
slot: 'footer-right', slot: 'footer-right',
class: 'right-4 bottom-4' class: 'right-4 bottom-4'
} : { }
: {
name: '#right', name: '#right',
class: 'right-4 inset-y-4 w-64' class: 'right-4 inset-y-4 w-64'
}] }]

View File

@@ -50,7 +50,7 @@ const dates = computed(() => {
const days = eachDayOfInterval({ start: new Date(first.published_at), end: new Date() }) const days = eachDayOfInterval({ start: new Date(first.published_at), end: new Date() })
return days.reverse().map(day => { return days.reverse().map((day) => {
return { return {
day, day,
release: releases.value.find(release => isSameDay(new Date(release.published_at), day)), release: releases.value.find(release => isSameDay(new Date(release.published_at), day)),

View File

@@ -1,13 +1,14 @@
import type { Options } from 'prettier' import type { Options } from 'prettier'
import { defu } from 'defu'
import PrettierWorker from '@/workers/prettier.js?worker&inline' import PrettierWorker from '@/workers/prettier.js?worker&inline'
export interface SimplePrettier { export interface SimplePrettier {
format: (source: string, options?: Options) => Promise<string>; format: (source: string, options?: Options) => Promise<string>
} }
function createPrettierWorkerApi(worker: Worker): SimplePrettier { function createPrettierWorkerApi(worker: Worker): SimplePrettier {
let counter = 0 let counter = 0
const handlers = {} const handlers: any = {}
worker.addEventListener('message', (event) => { worker.addEventListener('message', (event) => {
const { uid, message, error } = event.data const { uid, message, error } = event.data
@@ -17,6 +18,7 @@ function createPrettierWorkerApi (worker: Worker): SimplePrettier {
} }
const [resolve, reject] = handlers[uid] const [resolve, reject] = handlers[uid]
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete handlers[uid] delete handlers[uid]
if (error) { if (error) {
@@ -26,7 +28,7 @@ function createPrettierWorkerApi (worker: Worker): SimplePrettier {
} }
}) })
function postMessage<T> (message) { function postMessage<T>(message: any) {
const uid = ++counter const uid = ++counter
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
handlers[uid] = [resolve, reject] handlers[uid] = [resolve, reject]
@@ -41,16 +43,15 @@ function createPrettierWorkerApi (worker: Worker): SimplePrettier {
} }
} }
export default defineNuxtPlugin({ export default defineNuxtPlugin(async () => {
async setup () {
let prettier: SimplePrettier let prettier: SimplePrettier
if (import.meta.server) { if (import.meta.server) {
const prettierModule = await import('prettier') const prettierModule = await import('prettier')
prettier = { prettier = {
format (source, options = { format(source, options = {}) {
return prettierModule.format(source, defu(options, {
parser: 'markdown' parser: 'markdown'
}) { }))
return prettierModule.format(source, options)
} }
} }
} else { } else {
@@ -63,5 +64,4 @@ export default defineNuxtPlugin({
prettier prettier
} }
} }
}
}) })

View File

@@ -1,8 +1,4 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
import('https://unpkg.com/prettier@3.0.3/standalone.js')
import('https://unpkg.com/prettier@3.0.3/plugins/html.js')
import('https://unpkg.com/prettier@3.0.3/plugins/markdown.js')
self.onmessage = async function (event) { self.onmessage = async function (event) {
self.postMessage({ self.postMessage({
uid: event.data.uid, uid: event.data.uid,
@@ -18,6 +14,14 @@ function handleMessage (message) {
} }
async function handleFormatMessage(message) { async function handleFormatMessage(message) {
if (!globalThis.prettier) {
await Promise.all([
import('https://unpkg.com/prettier@3.3.3/standalone.js'),
import('https://unpkg.com/prettier@3.3.3/plugins/html.js'),
import('https://unpkg.com/prettier@3.3.3/plugins/markdown.js')
])
}
const { options, source } = message const { options, source } = message
const formatted = await prettier.format(source, { const formatted = await prettier.format(source, {
parser: 'markdown', parser: 'markdown',

19
eslint.config.mjs Normal file
View File

@@ -0,0 +1,19 @@
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
export default createConfigForNuxt({
features: {
tooling: true,
stylistic: {
commaDangle: 'never',
braceStyle: '1tbs'
}
}
}).overrideRules({
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': ['error', { singleline: 5 }],
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-explicit-any': 'off'
})

View File

@@ -1,8 +1,8 @@
{ {
"name": "@nuxt/ui", "name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.", "description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "2.18.7", "version": "2.19.2",
"packageManager": "pnpm@9.12.1", "packageManager": "pnpm@9.12.3",
"repository": "nuxt/ui", "repository": "nuxt/ui",
"homepage": "https://ui.nuxt.com", "homepage": "https://ui.nuxt.com",
"type": "module", "type": "module",
@@ -35,49 +35,51 @@
"@headlessui/tailwindcss": "^0.2.1", "@headlessui/tailwindcss": "^0.2.1",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@iconify-json/heroicons": "^1.2.1", "@iconify-json/heroicons": "^1.2.1",
"@nuxt/icon": "^1.5.5", "@nuxt/icon": "^1.6.1",
"@nuxt/kit": "^3.13.2", "@nuxt/kit": "^3.14.0",
"@nuxtjs/color-mode": "^3.5.1", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/tailwindcss": "^6.12.1", "@nuxtjs/tailwindcss": "^6.12.2",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@vueuse/core": "^11.1.0", "@vueuse/core": "^11.2.0",
"@vueuse/integrations": "^11.1.0", "@vueuse/integrations": "^11.2.0",
"@vueuse/math": "^11.1.0", "@vueuse/math": "^11.2.0",
"defu": "^6.1.4", "defu": "^6.1.4",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"scule": "^1.3.0", "scule": "^1.3.0",
"tailwind-merge": "^2.5.3", "tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.13" "tailwindcss": "^3.4.14"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint-config": "^0.4.0", "@nuxt/eslint-config": "^0.6.1",
"@nuxt/module-builder": "^0.8.4", "@nuxt/module-builder": "^0.8.4",
"@nuxt/test-utils": "^3.14.3", "@nuxt/test-utils": "^3.14.4",
"@release-it/conventional-changelog": "^8.0.2", "@release-it/conventional-changelog": "^9.0.2",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"eslint": "^8.57.0", "eslint": "^9.14.0",
"happy-dom": "^14.12.3", "happy-dom": "^14.12.3",
"joi": "^17.13.3", "joi": "^17.13.3",
"nuxt": "^3.13.2", "nuxt": "^3.14.0",
"release-it": "^17.7.0", "release-it": "^17.10.0",
"superstruct": "^2.0.2",
"unbuild": "^2.0.0", "unbuild": "^2.0.0",
"valibot": "^0.42.1", "valibot": "^0.42.1",
"valibot30": "npm:valibot@0.30.0", "valibot30": "npm:valibot@0.30.0",
"valibot31": "npm:valibot@0.31.0", "valibot31": "npm:valibot@0.31.0",
"vitest": "^2.1.2", "vitest": "^2.1.4",
"vitest-environment-nuxt": "^1.0.1", "vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.1.6", "vue-tsc": "^2.1.10",
"yup": "^1.4.0", "yup": "^1.4.0",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"resolutions": { "resolutions": {
"@nuxt/ui": "workspace:*", "@nuxt/ui": "workspace:*",
"@nuxt/content": "2.13.2",
"@nuxtjs/mdc": "0.9.0" "@nuxtjs/mdc": "0.9.0"
} }
} }

View File

@@ -9,6 +9,6 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/ui": "latest", "@nuxt/ui": "latest",
"nuxt": "^3.13.2" "nuxt": "^3.14.0"
} }
} }

5251
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,6 @@
"enabled": true "enabled": true
}, },
"ignoreDeps": [ "ignoreDeps": [
"@nuxt/eslint-config",
"eslint",
"happy-dom", "happy-dom",
"valibot30", "valibot30",
"valibot31" "valibot31"
@@ -23,5 +21,8 @@
"@tailwindcss/postcss", "@tailwindcss/postcss",
"@tailwindcss/vite" "@tailwindcss/vite"
] ]
}, {
"matchDepTypes": ["resolutions"],
"enabled": false
}] }]
} }

View File

@@ -1,6 +1,6 @@
import { promises as fsp } from 'fs' import { promises as fsp } from 'node:fs'
import { resolve } from 'path' import { resolve } from 'node:path'
import { execSync } from 'child_process' import { execSync } from 'node:child_process'
async function loadPackage(dir: string) { async function loadPackage(dir: string) {
const pkgPath = resolve(dir, 'package.json') const pkgPath = resolve(dir, 'package.json')
@@ -31,7 +31,6 @@ async function main () {
} }
main().catch((err) => { main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err) console.error(err)
process.exit(1) process.exit(1)
}) })

View File

@@ -2,7 +2,7 @@ import { createRequire } from 'node:module'
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin } from '@nuxt/kit' import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin } from '@nuxt/kit'
import { name, version } from '../package.json' import { name, version } from '../package.json'
import createTemplates from './templates' import createTemplates from './templates'
import * as config from './runtime/ui.config' import type * as config from './runtime/ui.config'
import type { DeepPartial, Strategy } from './runtime/types' import type { DeepPartial, Strategy } from './runtime/types'
import installTailwind from './tailwind' import installTailwind from './tailwind'
@@ -21,7 +21,7 @@ type UI = {
colors?: string[] colors?: string[]
strategy?: Strategy strategy?: Strategy
[key: string]: any [key: string]: any
} & DeepPartial<typeof config, string> } & DeepPartial<typeof config, string | number | boolean>
declare module '@nuxt/schema' { declare module '@nuxt/schema' {
interface AppConfigInput { interface AppConfigInput {

View File

@@ -9,10 +9,16 @@
<thead :class="ui.thead"> <thead :class="ui.thead">
<tr :class="ui.tr.base"> <tr :class="ui.tr.base">
<th v-if="modelValue" scope="col" :class="ui.checkbox.padding"> <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" /> <UCheckbox
:model-value="isAllRowChecked"
:indeterminate="indeterminate"
v-bind="ui.default.checkbox"
aria-label="Select all"
@change="onChange"
/>
</th> </th>
<th v-if="$slots.expand" scope="col" :class="ui.tr.base"> <th v-if="expand" scope="col" :class="ui.tr.base">
<span class="sr-only">Expand</span> <span class="sr-only">Expand</span>
</th> </th>
@@ -44,7 +50,7 @@
</thead> </thead>
<tbody :class="ui.tbody"> <tbody :class="ui.tbody">
<tr v-if="loadingState && loading && !rows.length"> <tr v-if="loadingState && loading && !rows.length">
<td :colspan="columns.length + (modelValue ? 1 : 0) + ($slots.expand ? 1 : 0)"> <td :colspan="columns.length + (modelValue ? 1 : 0) + (expand ? 1 : 0)">
<slot name="loading-state"> <slot name="loading-state">
<div :class="ui.loadingState.wrapper"> <div :class="ui.loadingState.wrapper">
<UIcon v-if="loadingState.icon" :name="loadingState.icon" :class="ui.loadingState.icon" aria-hidden="true" /> <UIcon v-if="loadingState.icon" :name="loadingState.icon" :class="ui.loadingState.icon" aria-hidden="true" />
@@ -57,7 +63,7 @@
</tr> </tr>
<tr v-else-if="emptyState && !rows.length"> <tr v-else-if="emptyState && !rows.length">
<td :colspan="columns.length + (modelValue ? 1 : 0) + ($slots.expand ? 1 : 0)"> <td :colspan="columns.length + (modelValue ? 1 : 0) + (expand ? 1 : 0)">
<slot name="empty-state"> <slot name="empty-state">
<div :class="ui.emptyState.wrapper"> <div :class="ui.emptyState.wrapper">
<UIcon v-if="emptyState.icon" :name="emptyState.icon" :class="ui.emptyState.icon" aria-hidden="true" /> <UIcon v-if="emptyState.icon" :name="emptyState.icon" :class="ui.emptyState.icon" aria-hidden="true" />
@@ -71,29 +77,38 @@
<template v-else> <template v-else>
<template v-for="(row, index) in rows" :key="index"> <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)"> <tr :class="[ui.tr.base, isSelected(row) && ui.tr.selected, isExpanded(row) && ui.tr.expanded, $attrs.onSelect && ui.tr.active, row?.class]" @click="() => onSelect(row)">
<td v-if="modelValue" :class="ui.checkbox.padding"> <td v-if="modelValue" :class="ui.checkbox.padding">
<UCheckbox v-model="selected" :value="row" v-bind="ui.default.checkbox" aria-label="Select row" @click.capture.stop="() => onSelect(row)" /> <UCheckbox
</td> :model-value="isSelected(row)"
v-bind="ui.default.checkbox"
<td aria-label="Select row"
v-if="$slots.expand" @change="onChangeCheckbox($event, row)"
:class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]" @click.capture.stop="() => onSelect(row)"
> />
<UButton </td>
v-bind="{ ...(ui.default.expandButton || {}), ...expandButton }" <td
:ui="{ icon: { base: [ui.expand.icon, openedRows.includes(index) && 'rotate-180'].join(' ') } }" v-if="expand"
@click="toggleOpened(index)" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]"
>
<template v-if="$slots['expand-action']">
<slot name="expand-action" :row="row" :is-expanded="isExpanded(row)" :toggle="() => toggleOpened(row)" />
</template>
<UButton
v-else
:disabled="row.disabledExpand"
v-bind="{ ...(ui.default.expandButton || {}), ...expandButton }"
:ui="{ icon: { base: [ui.expand.icon, isExpanded(row) && 'rotate-180'].join(' ') } }"
@click.capture.stop="toggleOpened(row)"
/> />
</td> </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]"> <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)"> <slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)">
{{ getRowData(row, column.key) }} {{ getRowData(row, column.key) }}
</slot> </slot>
</td> </td>
</tr> </tr>
<tr v-if="openedRows.includes(index)"> <tr v-if="isExpanded(row)">
<td colspan="100%"> <td colspan="100%">
<slot <slot
name="expand" name="expand"
@@ -110,7 +125,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, computed, defineComponent, toRaw, toRef } from 'vue' import { computed, defineComponent, toRaw, toRef } from 'vue'
import type { PropType, AriaAttributes } from 'vue' import type { PropType, AriaAttributes } from 'vue'
import { upperFirst } from 'scule' import { upperFirst } from 'scule'
import { defu } from 'defu' import { defu } from 'defu'
@@ -121,7 +136,7 @@ import UProgress from '../elements/Progress.vue'
import UCheckbox from '../forms/Checkbox.vue' import UCheckbox from '../forms/Checkbox.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig, get } from '../../utils' import { mergeConfig, get } from '../../utils'
import type { Strategy, Button, ProgressColor, ProgressAnimation, DeepPartial } from '../../types/index' import type { TableRow, TableColumn, Strategy, Button, ProgressColor, ProgressAnimation, DeepPartial, Expanded } from '../../types/index'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { table } from '#ui/ui.config' import { table } from '#ui/ui.config'
@@ -129,7 +144,7 @@ import { table } from '#ui/ui.config'
const config = mergeConfig<typeof table>(appConfig.ui.strategy, appConfig.ui.table, table) const config = mergeConfig<typeof table>(appConfig.ui.strategy, appConfig.ui.table, table)
function defaultComparator<T>(a: T, z: T): boolean { function defaultComparator<T>(a: T, z: T): boolean {
return a === z return JSON.stringify(a) === JSON.stringify(z)
} }
function defaultSort(a: any, b: any, direction: 'asc' | 'desc') { function defaultSort(a: any, b: any, direction: 'asc' | 'desc') {
@@ -144,16 +159,6 @@ 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({ export default defineComponent({
components: { components: {
UIcon, UIcon,
@@ -172,11 +177,11 @@ export default defineComponent({
default: () => defaultComparator default: () => defaultComparator
}, },
rows: { rows: {
type: Array as PropType<{ [key: string]: any }[]>, type: Array as PropType<TableRow[]>,
default: () => [] default: () => []
}, },
columns: { columns: {
type: Array as PropType<Column[]>, type: Array as PropType<TableColumn[]>,
default: null default: null
}, },
columnAttribute: { columnAttribute: {
@@ -207,6 +212,10 @@ export default defineComponent({
type: Object as PropType<Button>, type: Object as PropType<Button>,
default: () => config.default.expandButton as Button default: () => config.default.expandButton as Button
}, },
expand: {
type: Object as PropType<Expanded<TableRow>>,
default: () => null
},
loading: { loading: {
type: Boolean, type: Boolean,
default: false default: false
@@ -234,17 +243,26 @@ export default defineComponent({
ui: { ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>, type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}) default: () => ({})
},
multipleExpand: {
type: Boolean,
default: true
} }
}, },
emits: ['update:modelValue', 'update:sort'], emits: ['update:modelValue', 'update:sort', 'update:expand'],
setup(props, { emit, attrs: $attrs }) { setup(props, { emit, attrs: $attrs }) {
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class')) 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 }) as Column)) const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map(key => ({ key, label: upperFirst(key), sortable: false, class: undefined, sort: defaultSort }) as TableColumn))
const sort = useVModel(props, 'sort', emit, { passive: true, defaultValue: defu({}, props.sort, { column: null, direction: 'asc' }) }) const sort = useVModel(props, 'sort', emit, { passive: true, defaultValue: defu({}, props.sort, { column: null, direction: 'asc' }) })
const expand = useVModel(props, 'expand', emit, {
const openedRows = ref([]) passive: true,
defaultValue: defu({}, props.expand, {
openedRows: [],
row: null
})
})
const savedSort = { column: sort.value.column, direction: null } const savedSort = { column: sort.value.column, direction: null }
@@ -259,7 +277,7 @@ export default defineComponent({
const aValue = get(a, column) const aValue = get(a, column)
const bValue = get(b, column) const bValue = get(b, column)
const sort = columns.value.find((col) => col.key === column)?.sort ?? defaultSort const sort = columns.value.find(col => col.key === column)?.sort ?? defaultSort
return sort(aValue, bValue, direction) return sort(aValue, bValue, direction)
}) })
@@ -274,7 +292,23 @@ export default defineComponent({
} }
}) })
const indeterminate = computed(() => selected.value && selected.value.length > 0 && selected.value.length < props.rows.length) const getStringifiedSet = (arr: TableRow[]) => new Set(arr.map(item => JSON.stringify(item)))
const totalRows = computed(() => props.rows.length)
const countCheckedRow = computed(() => {
const selectedData = getStringifiedSet(selected.value)
const rowsData = getStringifiedSet(props.rows)
return Array.from(selectedData).filter(item => rowsData.has(item)).length
})
const indeterminate = computed(() => {
if (!selected.value || !props.rows) return false
return countCheckedRow.value > 0 && countCheckedRow.value < totalRows.value
})
const isAllRowChecked = computed(() => countCheckedRow.value === totalRows.value)
const emptyState = computed(() => { const emptyState = computed(() => {
if (props.emptyState === null) return null if (props.emptyState === null) return null
@@ -288,18 +322,22 @@ export default defineComponent({
function compare(a: any, z: any) { function compare(a: any, z: any) {
if (typeof props.by === 'string') { if (typeof props.by === 'string') {
const property = props.by as unknown as any const accesorFn = accessor(props.by)
return a?.[property] === z?.[property] return accesorFn(a) === accesorFn(z)
} }
return props.by(a, z) return props.by(a, z)
} }
function isSelected (row) { function accessor<T extends Record<string, any>>(key: string) {
return (obj: T) => get(obj, key)
}
function isSelected(row: TableRow) {
if (!props.modelValue) { if (!props.modelValue) {
return false return false
} }
return selected.value.some((item) => compare(toRaw(item), toRaw(row))) return selected.value.some(item => compare(toRaw(item), toRaw(row)))
} }
function onSort(column: { key: string, direction?: 'asc' | 'desc' }) { function onSort(column: { key: string, direction?: 'asc' | 'desc' }) {
@@ -316,7 +354,7 @@ export default defineComponent({
} }
} }
function onSelect (row) { function onSelect(row: TableRow) {
if (!$attrs.onSelect) { if (!$attrs.onSelect) {
return return
} }
@@ -348,19 +386,31 @@ export default defineComponent({
} }
} }
function getRowData (row: Object, rowKey: string | string[], defaultValue: any = '') { function onChangeCheckbox(checked: boolean, row: TableRow) {
if (checked) {
selected.value.push(row)
} else {
const index = selected.value.findIndex(item => compare(item, row))
selected.value.splice(index, 1)
}
}
function getRowData(row: TableRow, rowKey: string | string[], defaultValue: any = '') {
return get(row, rowKey, defaultValue) return get(row, rowKey, defaultValue)
} }
function toggleOpened (index: number) { function isExpanded(row: TableRow) {
if (openedRows.value.includes(index)) { return expand.value?.openedRows ? expand.value.openedRows.some(openedRow => compare(openedRow, row)) : false
openedRows.value = openedRows.value.filter((i) => i !== index) }
} else {
openedRows.value.push(index) function toggleOpened(row: TableRow) {
expand.value = {
openedRows: isExpanded(row) ? expand.value.openedRows.filter(v => !compare(v, row)) : props.multipleExpand ? [...expand.value.openedRows, row] : [row],
row
} }
} }
function getAriaSort (column: Column): AriaAttributes['aria-sort'] { function getAriaSort(column: TableColumn): AriaAttributes['aria-sort'] {
if (!column.sortable) { if (!column.sortable) {
return undefined return undefined
} }
@@ -396,14 +446,16 @@ export default defineComponent({
emptyState, emptyState,
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
loadingState, loadingState,
openedRows, isAllRowChecked,
onChangeCheckbox,
isSelected, isSelected,
onSort, onSort,
onSelect, onSelect,
onChange, onChange,
getRowData, getRowData,
toggleOpened, toggleOpened,
getAriaSort getAriaSort,
isExpanded
} }
} }
}) })

View File

@@ -161,6 +161,7 @@ export default defineComponent({
function onEnter(_el: Element, done: () => void) { function onEnter(_el: Element, done: () => void) {
const el = _el as HTMLElement const el = _el as HTMLElement
el.style.height = '0' el.style.height = '0'
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
el.offsetHeight // Trigger a reflow, flushing the CSS changes el.offsetHeight // Trigger a reflow, flushing the CSS changes
el.style.height = el.scrollHeight + 'px' el.style.height = el.scrollHeight + 'px'
@@ -170,6 +171,7 @@ export default defineComponent({
function onBeforeLeave(_el: Element) { function onBeforeLeave(_el: Element) {
const el = _el as HTMLElement const el = _el as HTMLElement
el.style.height = el.scrollHeight + 'px' el.style.height = el.scrollHeight + 'px'
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
el.offsetHeight // Trigger a reflow, flushing the CSS changes el.offsetHeight // Trigger a reflow, flushing the CSS changes
} }

View File

@@ -1,10 +1,10 @@
import { h, cloneVNode, computed, toRef, defineComponent } from 'vue' import { h, cloneVNode, computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import UAvatar from './Avatar.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig, getSlotsChildren } from '../../utils' import { mergeConfig, getSlotsChildren } from '../../utils'
import type { AvatarSize, Strategy } from '../../types/index' import type { AvatarSize, Strategy } from '../../types/index'
import UAvatar from './Avatar.vue'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { avatar, avatarGroup } from '#ui/ui.config' import { avatar, avatarGroup } from '#ui/ui.config'
@@ -41,7 +41,7 @@ export default defineComponent({
const children = computed(() => getSlotsChildren(slots)) const children = computed(() => getSlotsChildren(slots))
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max) const max = computed(() => typeof props.max === 'string' ? Number.parseInt(props.max, 10) : props.max)
const clones = computed(() => children.value.map((node, index) => { const clones = computed(() => children.value.map((node, index) => {
const vProps: any = {} const vProps: any = {}

View File

@@ -59,12 +59,12 @@
import { ref, toRef, computed, defineComponent } from 'vue' import { ref, toRef, computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import { useScroll, useResizeObserver, useElementSize } from '@vueuse/core'
import { mergeConfig } from '../../utils' import { mergeConfig } from '../../utils'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import type { Strategy, Button, DeepPartial } from '../../types/index' import type { Strategy, Button, DeepPartial } from '../../types/index'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useCarouselScroll } from '../../composables/useCarouselScroll' import { useCarouselScroll } from '../../composables/useCarouselScroll'
import { useScroll, useResizeObserver, useElementSize } from '@vueuse/core'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { carousel } from '#ui/ui.config' import { carousel } from '#ui/ui.config'

View File

@@ -209,7 +209,9 @@ export default defineComponent({
return return
} }
openTimeout = openTimeout || setTimeout(() => { openTimeout = openTimeout || setTimeout(() => {
menuApi.value.openMenu && menuApi.value.openMenu() if (menuApi.value.openMenu) {
menuApi.value.openMenu()
}
openTimeout = null openTimeout = null
}, props.openDelay) }, props.openDelay)
} }
@@ -229,7 +231,9 @@ export default defineComponent({
return return
} }
closeTimeout = closeTimeout || setTimeout(() => { closeTimeout = closeTimeout || setTimeout(() => {
menuApi.value.closeMenu && menuApi.value.closeMenu() if (menuApi.value.closeMenu) {
menuApi.value.closeMenu()
}
closeTimeout = null closeTimeout = null
}, props.closeDelay) }, props.closeDelay)
} }

View File

@@ -47,8 +47,8 @@ export default defineComponent({
}, },
inheritAttrs: false, inheritAttrs: false,
slots: Object as SlotsType<{ slots: Object as SlotsType<{
indicator?: { percent: number, value: number }, indicator?: { percent: number, value: number }
label?: { percent: number, value: number }, label?: { percent: number, value: number }
}>, }>,
props: { props: {
value: { value: {

View File

@@ -2,10 +2,10 @@ import { h, cloneVNode, computed, toRef, defineComponent } from 'vue'
import type { ComputedRef, VNode, SlotsType, PropType } from 'vue' import type { ComputedRef, VNode, SlotsType, PropType } from 'vue'
import { twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import Meter from './Meter.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig, getSlotsChildren } from '../../utils' import { mergeConfig, getSlotsChildren } from '../../utils'
import type { Strategy, MeterSize } from '../../types/index' import type { Strategy, MeterSize } from '../../types/index'
import type Meter from './Meter.vue'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { meter, meterGroup } from '#ui/ui.config' import { meter, meterGroup } from '#ui/ui.config'
@@ -19,8 +19,8 @@ export default defineComponent({
}, },
inheritAttrs: false, inheritAttrs: false,
slots: Object as SlotsType<{ slots: Object as SlotsType<{
default?: typeof Meter[], default?: typeof Meter[]
indicator?: { percent: number }, indicator?: { percent: number }
}>, }>,
props: { props: {
min: { min: {

View File

@@ -256,6 +256,20 @@ progress:indeterminate {
} }
} }
[dir=rtl] &.bar-animation-carousel {
&:after {
animation: carousel-rtl 2s ease-in-out infinite;
}
&::-webkit-progress-value {
animation: carousel-rtl 2s ease-in-out infinite;
}
&::-moz-progress-bar {
animation: carousel-rtl 2s ease-in-out infinite;
}
}
&.bar-animation-carousel-inverse { &.bar-animation-carousel-inverse {
&:after { &:after {
animation: carousel-inverse 2s ease-in-out infinite; animation: carousel-inverse 2s ease-in-out infinite;
@@ -270,6 +284,20 @@ progress:indeterminate {
} }
} }
[dir=rtl] &.bar-animation-carousel-inverse {
&:after {
animation: carousel-inverse-rtl 2s ease-in-out infinite;
}
&::-webkit-progress-value {
animation: carousel-inverse-rtl 2s ease-in-out infinite;
}
&::-moz-progress-bar {
animation: carousel-inverse-rtl 2s ease-in-out infinite;
}
}
&.bar-animation-swing { &.bar-animation-swing {
&:after { &:after {
animation: swing 3s ease-in-out infinite; animation: swing 3s ease-in-out infinite;
@@ -315,6 +343,22 @@ progress:indeterminate {
} }
} }
@keyframes carousel-rtl {
0%,
100% {
width: 50%
}
0% {
transform: translateX(100%)
}
100% {
transform: translateX(-200%)
}
}
@keyframes carousel-inverse { @keyframes carousel-inverse {
0%, 0%,
@@ -331,6 +375,22 @@ progress:indeterminate {
} }
} }
@keyframes carousel-inverse-rtl {
0%,
100% {
width: 50%
}
0% {
transform: translateX(-200%)
}
100% {
transform: translateX(100%)
}
}
@keyframes swing { @keyframes swing {
0%, 0%,
@@ -361,4 +421,5 @@ progress:indeterminate {
width: 90%; width: 90%;
margin-left: 5% margin-left: 5%
} }
}</style> }
</style>

View File

@@ -40,7 +40,7 @@ import type { DeepPartial, Strategy } from '../../types/index'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { checkbox } from '#ui/ui.config' import { checkbox } from '#ui/ui.config'
import colors from '#ui-colors' import type colors from '#ui-colors'
import { useId } from '#app' import { useId } from '#app'
const config = mergeConfig<typeof checkbox>(appConfig.ui.strategy, appConfig.ui.checkbox, checkbox) const config = mergeConfig<typeof checkbox>(appConfig.ui.strategy, appConfig.ui.checkbox, checkbox)

View File

@@ -13,7 +13,8 @@ import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } fro
import type { BaseSchema as ValibotSchema30, BaseSchemaAsync as ValibotSchemaAsync30 } from 'valibot30' 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 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 { 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 type { Struct } from 'superstruct'
import type { FormError, FormEvent, FormEventType, FormSubmitEvent, FormErrorEvent, Form, ValidateReturnSchema } from '../../types/form'
import { useId } from '#imports' import { useId } from '#imports'
class FormException extends Error { class FormException extends Error {
@@ -24,18 +25,19 @@ class FormException extends Error {
} }
} }
export default defineComponent({ type Schema = PropType<ZodSchema>
props: {
schema: {
type: [Object, Function] as
| PropType<ZodSchema>
| PropType<YupObjectSchema<any>> | PropType<YupObjectSchema<any>>
| PropType<JoiSchema> | PropType<JoiSchema>
| PropType<ValibotSchema30 | ValibotSchemaAsync30> | PropType<ValibotSchema30 | ValibotSchemaAsync30>
| PropType<ValibotSchema31 | ValibotSchemaAsync31> | PropType<ValibotSchema31 | ValibotSchemaAsync31>
| PropType<ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any>> | PropType<ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any>>
| PropType<ValibotSchema | ValibotSchemaAsync> | PropType<ValibotSchema | ValibotSchemaAsync>
| PropType<ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>>, | PropType<ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>> | PropType<Struct<any, any>>
export default defineComponent({
props: {
schema: {
type: [Object, Function] as Schema,
default: undefined default: undefined
}, },
state: { state: {
@@ -71,6 +73,7 @@ export default defineComponent({
}) })
const errors = ref<FormError[]>([]) const errors = ref<FormError[]>([])
provide('form-errors', errors) provide('form-errors', errors)
provide('form-events', bus) provide('form-events', bus)
const inputs = ref({}) const inputs = ref({})
@@ -80,16 +83,11 @@ export default defineComponent({
let errs = await props.validate(props.state) let errs = await props.validate(props.state)
if (props.schema) { if (props.schema) {
if (isZodSchema(props.schema)) { const { errors, result } = await parseSchema(props.state, props.schema as unknown as Schema)
errs = errs.concat(await getZodErrors(props.state, props.schema)) if (errors) {
} else if (isYupSchema(props.schema)) { errs = errs.concat(errors)
errs = errs.concat(await getYupErrors(props.state, props.schema))
} else if (isJoiSchema(props.schema)) {
errs = errs.concat(await getJoiErrors(props.state, props.schema))
} else if (isValibotSchema(props.schema)) {
errs = errs.concat(await getValibotError(props.state, props.schema))
} else { } else {
throw new Error('Form validation failed: Unsupported form schema') Object.assign(props.state, result)
} }
} }
@@ -105,10 +103,10 @@ export default defineComponent({
if (paths) { if (paths) {
const otherErrors = errors.value.filter( const otherErrors = errors.value.filter(
(error) => !paths.includes(error.path) error => !paths.includes(error.path)
) )
const pathErrors = (await getErrors()).filter( const pathErrors = (await getErrors()).filter(
(error) => paths.includes(error.path) error => paths.includes(error.path)
) )
errors.value = otherErrors.concat(pathErrors) errors.value = otherErrors.concat(pathErrors)
} else { } else {
@@ -141,7 +139,7 @@ export default defineComponent({
const errorEvent: FormErrorEvent = { const errorEvent: FormErrorEvent = {
...event, ...event,
errors: errors.value.map((err) => ({ errors: errors.value.map(err => ({
...err, ...err,
id: inputs.value[err.path] id: inputs.value[err.path]
})) }))
@@ -156,7 +154,7 @@ export default defineComponent({
setErrors(errs: FormError[], path?: string) { setErrors(errs: FormError[], path?: string) {
if (path) { if (path) {
errors.value = errors.value.filter( errors.value = errors.value.filter(
(error) => error.path !== path error => error.path !== path
).concat(errs) ).concat(errs)
} else { } else {
errors.value = errs errors.value = errs
@@ -167,13 +165,13 @@ export default defineComponent({
}, },
getErrors(path?: string) { getErrors(path?: string) {
if (path) { if (path) {
return errors.value.filter((err) => err.path === path) return errors.value.filter(err => err.path === path)
} }
return errors.value return errors.value
}, },
clear(path?: string) { clear(path?: string) {
if (path) { if (path) {
errors.value = errors.value.filter((err) => err.path !== path) errors.value = errors.value.filter(err => err.path !== path)
} else { } else {
errors.value = [] errors.value = []
} }
@@ -195,41 +193,13 @@ function isYupError (error: any): error is YupError {
return error.inner !== undefined return error.inner !== undefined
} }
async function getYupErrors ( function isSuperStructSchema(schema: any): schema is Struct<any, any> {
state: any, return (
schema: YupObjectSchema<any> 'schema' in schema
): Promise<FormError[]> { && typeof schema.coercer === 'function'
try { && typeof schema.validator === 'function'
await schema.validate(state, { abortEarly: false }) && typeof schema.refiner === 'function'
return [] )
} catch (error) {
if (isYupError(error)) {
return error.inner.map((issue) => ({
path: issue.path ?? '',
message: issue.message
}))
} else {
throw error
}
}
}
function isZodSchema (schema: any): schema is ZodSchema {
return schema.parse !== undefined
}
async function getZodErrors (
state: any,
schema: ZodSchema
): Promise<FormError[]> {
const result = await schema.safeParseAsync(state)
if (result.success === false) {
return result.error.issues.map((issue) => ({
path: issue.path.join('.'),
message: issue.message
}))
}
return []
} }
function isJoiSchema(schema: any): schema is JoiSchema { function isJoiSchema(schema: any): schema is JoiSchema {
@@ -240,38 +210,152 @@ function isJoiError (error: any): error is JoiError {
return error.isJoi === true return error.isJoi === true
} }
async function getJoiErrors ( 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)
}
function isZodSchema(schema: any): schema is ZodSchema {
return schema.parse !== undefined
}
async function validateValibotSchema(
state: any,
schema: ValibotSchema30 | ValibotSchemaAsync30 | ValibotSchema31 | ValibotSchemaAsync31 | ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any> | ValibotSchema | ValibotSchemaAsync | ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>
): Promise<ValidateReturnSchema<typeof state>> {
const result = await ('_parse' in schema ? schema._parse(state) : '_run' in schema ? schema._run({ typed: false, value: state }, {}) : schema(state))
if (!result.issues || result.issues.length === 0) {
const output = ('output' in result
? result.output
: 'value' in result
? result.value
: null)
return {
errors: null,
result: output
}
}
const errors = result.issues.map(issue => ({
path: issue.path?.map(item => item.key).join('.') || '',
message: issue.message
}))
return {
errors,
result: null
}
}
async function validateJoiSchema(
state: any, state: any,
schema: JoiSchema schema: JoiSchema
): Promise<FormError[]> { ): Promise<ValidateReturnSchema<typeof state>> {
try { try {
await schema.validateAsync(state, { abortEarly: false }) await schema.validateAsync(state, { abortEarly: false })
return [] return {
errors: null,
result: state
}
} catch (error) { } catch (error) {
if (isJoiError(error)) { if (isJoiError(error)) {
return error.details.map((detail) => ({ const errors = error.details.map(issue => ({
path: detail.path.join('.'), path: issue.path.join('.'),
message: detail.message message: issue.message
})) }))
return {
errors,
result: null
}
} else { } else {
throw error throw error
} }
} }
} }
function isValibotSchema (schema: any): schema is ValibotSchema30 | ValibotSchemaAsync30 | ValibotSchema31 | ValibotSchemaAsync31 | ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any> | ValibotSchema | ValibotSchemaAsync | ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any> { async function validateZodSchema(
return '_parse' in schema || '_run' in schema || (typeof schema === 'function' && 'schema' in schema) state: any,
schema: ZodSchema
): Promise<ValidateReturnSchema<typeof state>> {
const result = await schema.safeParseAsync(state)
if (result.success === false) {
const errors = result.error.issues.map(issue => ({
path: issue.path.join('.'),
message: issue.message
}))
return {
errors,
result: null
}
}
return {
result: result.data,
errors: null
}
} }
async function getValibotError ( async function validateSuperstructSchema(state: any, schema: Struct<any, any>): Promise<ValidateReturnSchema<typeof state>> {
const [err, result] = schema.validate(state)
if (err) {
const errors = err.failures().map(error => ({
message: error.message,
path: error.path.join('.')
}))
return {
errors,
result: null
}
}
return {
errors: null,
result
}
}
async function validateYupSchema(
state: any, state: any,
schema: ValibotSchema30 | ValibotSchemaAsync30 | ValibotSchema31 | ValibotSchemaAsync31 | ValibotSafeParser31<any, any> | ValibotSafeParserAsync31<any, any> | ValibotSchema | ValibotSchemaAsync | ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any> schema: YupObjectSchema<any>
): Promise<FormError[]> { ): Promise<ValidateReturnSchema<typeof state>> {
const result = await ('_parse' in schema ? schema._parse(state) : '_run' in schema ? schema._run({ typed: false, value: state }, {}) : schema(state)) try {
return result.issues?.map((issue) => ({ const result = schema.validateSync(state, { abortEarly: false })
// We know that the key for a form schema is always a string or a number return {
path: issue.path?.map((item) => item.key).join('.') || '', errors: null,
result
}
} catch (error) {
if (isYupError(error)) {
const errors = error.inner.map(issue => ({
path: issue.path ?? '',
message: issue.message message: issue.message
})) || [] }))
return {
errors,
result: null
}
} else {
throw error
}
}
}
function parseSchema(state: any, schema: Schema): Promise<ValidateReturnSchema<typeof state>> {
if (isZodSchema(schema)) {
return validateZodSchema(state, schema)
} else if (isJoiSchema(schema)) {
return validateJoiSchema(state, schema)
} else if (isValibotSchema(schema)) {
return validateValibotSchema(state, schema)
} else if (isYupSchema(schema)) {
return validateYupSchema(state, schema)
} else if (isSuperStructSchema(schema)) {
return validateSuperstructSchema(state, schema)
} else {
throw new Error('Form validation failed: Unsupported form schema')
}
} }
</script> </script>

View File

@@ -111,7 +111,7 @@ export default defineComponent({
const error = computed(() => { const error = computed(() => {
return (props.error && typeof props.error === 'string') || typeof props.error === 'boolean' return (props.error && typeof props.error === 'string') || typeof props.error === 'boolean'
? props.error ? props.error
: formErrors?.value?.find((error) => error.path === props.name)?.message : formErrors?.value?.find(error => error.path === props.name)?.message
}) })
const size = computed(() => ui.value.size[props.size ?? config.default.size]) const size = computed(() => ui.value.size[props.size ?? config.default.size])

View File

@@ -34,8 +34,8 @@
import { ref, computed, toRef, onMounted, defineComponent } from 'vue' import { ref, computed, toRef, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { defu } from 'defu' import { defu } from 'defu'
import UIcon from '../elements/Icon.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup' import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig, looseToNumber } from '../../utils' import { mergeConfig, looseToNumber } from '../../utils'
@@ -158,7 +158,7 @@ export default defineComponent({
default: () => ({}) default: () => ({})
}, },
modelModifiers: { modelModifiers: {
type: Object as PropType<{ trim?: boolean, lazy?: boolean, number?: boolean }>, type: Object as PropType<{ trim?: boolean, lazy?: boolean, number?: boolean, nullify?: boolean }>,
default: () => ({}) default: () => ({})
} }
}, },
@@ -172,7 +172,7 @@ export default defineComponent({
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 })) const modelModifiers = ref(defu({}, props.modelModifiers, { trim: false, lazy: false, number: false, nullify: false }))
const input = ref<HTMLInputElement | null>(null) const input = ref<HTMLInputElement | null>(null)
@@ -184,7 +184,6 @@ export default defineComponent({
// Custom function to handle the v-model properties // Custom function to handle the v-model properties
const updateInput = (value: string) => { const updateInput = (value: string) => {
if (modelModifiers.value.trim) { if (modelModifiers.value.trim) {
value = value.trim() value = value.trim()
} }
@@ -193,6 +192,10 @@ export default defineComponent({
value = looseToNumber(value) value = looseToNumber(value)
} }
if (modelModifiers.value.nullify) {
value ||= null
}
emit('update:modelValue', value) emit('update:modelValue', value)
emitFormInput() emitFormInput()
} }

View File

@@ -63,7 +63,7 @@
/> />
<span v-else-if="option.chip" :class="uiMenu.option.chip.base" :style="{ background: `#${option.chip}` }" /> <span v-else-if="option.chip" :class="uiMenu.option.chip.base" :style="{ background: `#${option.chip}` }" />
<span class="truncate">{{ ['string', 'number'].includes(typeof option) ? option : option[optionAttribute] }}</span> <span class="truncate">{{ ['string', 'number'].includes(typeof option) ? option : accessor(option, optionAttribute) }}</span>
</slot> </slot>
</div> </div>
@@ -75,12 +75,12 @@
<p v-if="query && !filteredOptions.length" :class="uiMenu.option.empty"> <p v-if="query && !filteredOptions.length" :class="uiMenu.option.empty">
<slot name="option-empty" :query="query"> <slot name="option-empty" :query="query">
No results for "{{ query }}". {{ uiMenu.default.optionEmpty.label.replace('{query}', query) }}
</slot> </slot>
</p> </p>
<p v-else-if="!filteredOptions.length" :class="uiMenu.empty"> <p v-else-if="!filteredOptions.length" :class="uiMenu.empty">
<slot name="empty" :query="query"> <slot name="empty" :query="query">
No options. {{ uiMenu.default.empty.label }}
</slot> </slot>
</p> </p>
</HComboboxOptions> </HComboboxOptions>
@@ -91,7 +91,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, computed, toRef, watch, defineComponent } from 'vue' import { ref, computed, toRef, watch, defineComponent, toRaw } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { import {
Combobox as HCombobox, Combobox as HCombobox,
@@ -310,9 +310,9 @@ export default defineComponent({
if (props.valueAttribute) { if (props.valueAttribute) {
const option = options.value.find(option => option[props.valueAttribute] === props.modelValue) const option = options.value.find(option => option[props.valueAttribute] === props.modelValue)
return option ? option[props.optionAttribute] : null return option ? accessor(option, props.optionAttribute) : null
} else { } else {
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : props.modelValue[props.optionAttribute] return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : accessor(props.modelValue as Record<string, any>, props.optionAttribute)
} }
}) })
@@ -401,20 +401,26 @@ export default defineComponent({
lazy: props.searchLazy lazy: props.searchLazy
}) })
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, match => `\\${match}`)
}
const filteredOptions = computed(() => { const filteredOptions = computed(() => {
if (!query.value || debouncedSearch) { if (!query.value || debouncedSearch) {
return options.value return options.value
} }
const escapedQuery = escapeRegExp(query.value)
return options.value.filter((option: any) => { return options.value.filter((option: any) => {
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => { return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => {
if (['string', 'number'].includes(typeof option)) { if (['string', 'number'].includes(typeof option)) {
return String(option).search(new RegExp(query.value, 'i')) !== -1 return String(option).search(new RegExp(escapedQuery, 'i')) !== -1
} }
const child = get(option, searchAttribute) const child = get(option, searchAttribute)
return child !== null && child !== undefined && String(child).search(new RegExp(query.value, 'i')) !== -1 return child !== null && child !== undefined && String(child).search(new RegExp(escapedQuery, 'i')) !== -1
}) })
}) })
}) })
@@ -430,12 +436,21 @@ export default defineComponent({
function onUpdate(value: any) { function onUpdate(value: any) {
query.value = '' query.value = ''
if (toRaw(props.modelValue) === toRaw(value)) {
return
}
emit('update:modelValue', value) emit('update:modelValue', value)
emit('change', value) emit('change', value)
emitFormChange() emitFormChange()
} }
function accessor<T extends Record<string, any>>(obj: T, key: string) {
return get(obj, key)
}
function onQueryChange(event: any) { function onQueryChange(event: any) {
query.value = event.target.value query.value = event.target.value
} }
@@ -469,6 +484,7 @@ export default defineComponent({
filteredOptions, filteredOptions,
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
query, query,
accessor,
onUpdate, onUpdate,
onQueryChange onQueryChange
} }

View File

@@ -39,7 +39,7 @@ import type { DeepPartial, Strategy } from '../../types/index'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { radio } from '#ui/ui.config' import { radio } from '#ui/ui.config'
import colors from '#ui-colors' import type colors from '#ui-colors'
import { useId } from '#imports' import { useId } from '#imports'
const config = mergeConfig<typeof radio>(appConfig.ui.strategy, appConfig.ui.radio, radio) const config = mergeConfig<typeof radio>(appConfig.ui.strategy, appConfig.ui.radio, radio)

View File

@@ -17,11 +17,11 @@
:ui="uiRadio" :ui="uiRadio"
@change="onUpdate(option.value)" @change="onUpdate(option.value)"
> >
<template #label> <template v-if="$slots.label" #label>
<slot name="label" v-bind="{ option, selected: option.selected }" /> <slot name="label" v-bind="{ option, selected: option.selected }" />
</template> </template>
<template #help> <template v-if="$slots.help" #help>
<slot name="help" v-bind="{ option, selected: option.selected }" /> <slot name="help" v-bind="{ option, selected: option.selected }" />
</template> </template>
</URadio> </URadio>
@@ -30,17 +30,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import URadio from './Radio.vue'
import { computed, defineComponent, provide, toRef } from 'vue' import { computed, defineComponent, provide, toRef } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup' import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig, get } from '../../utils' import { mergeConfig, get } from '../../utils'
import type { DeepPartial, Strategy } from '../../types/index' import type { DeepPartial, Strategy } from '../../types/index'
import URadio from './Radio.vue'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { radioGroup, radio } from '#ui/ui.config' import { radioGroup, radio } from '#ui/ui.config'
import colors from '#ui-colors' import type colors from '#ui-colors'
const config = mergeConfig<typeof radioGroup>(appConfig.ui.strategy, appConfig.ui.radioGroup, radioGroup) const config = mergeConfig<typeof radioGroup>(appConfig.ui.strategy, appConfig.ui.radioGroup, radioGroup)
const configRadio = mergeConfig<typeof radio>(appConfig.ui.strategy, appConfig.ui.radio, radio) const configRadio = mergeConfig<typeof radio>(appConfig.ui.strategy, appConfig.ui.radio, radio)
@@ -152,7 +152,7 @@ export default defineComponent({
uiRadio, uiRadio,
attrs, attrs,
normalizedOptions, normalizedOptions,
// eslint-disable-next-line vue/no-dupe-keys
onUpdate onUpdate
} }
} }

View File

@@ -86,7 +86,7 @@
/> />
<span v-else-if="option.chip" :class="uiMenu.option.chip.base" :style="{ background: `#${option.chip}` }" /> <span v-else-if="option.chip" :class="uiMenu.option.chip.base" :style="{ background: `#${option.chip}` }" />
<span class="truncate">{{ ['string', 'number'].includes(typeof option) ? option : option[optionAttribute] }}</span> <span class="truncate">{{ ['string', 'number'].includes(typeof option) ? option : accessor(option, optionAttribute) }}</span>
</slot> </slot>
</div> </div>
@@ -100,19 +100,19 @@
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive]"> <li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive]">
<div :class="uiMenu.option.container"> <div :class="uiMenu.option.container">
<slot name="option-create" :option="createOption" :active="active" :selected="optionSelected"> <slot name="option-create" :option="createOption" :active="active" :selected="optionSelected">
<span :class="uiMenu.option.create">Create "{{ createOption[optionAttribute] }}"</span> <span :class="uiMenu.option.create">Create "{{ typeof createOption === 'string' ? createOption : accessor(createOption, optionAttribute) }}"</span>
</slot> </slot>
</div> </div>
</li> </li>
</component> </component>
<p v-else-if="searchable && query && !filteredOptions?.length" :class="uiMenu.option.empty"> <p v-else-if="searchable && query && !filteredOptions?.length" :class="uiMenu.option.empty">
<slot name="option-empty" :query="query"> <slot name="option-empty" :query="query">
No results for "{{ query }}". {{ uiMenu.default.optionEmpty.label.replace('{query}', query) }}
</slot> </slot>
</p> </p>
<p v-else-if="!filteredOptions?.length" :class="uiMenu.empty"> <p v-else-if="!filteredOptions?.length" :class="uiMenu.empty">
<slot name="empty" :query="query"> <slot name="empty" :query="query">
No options. {{ uiMenu.default.empty.label }}
</slot> </slot>
</p> </p>
</component> </component>
@@ -123,7 +123,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, computed, toRef, watch, defineComponent } from 'vue' import { ref, computed, toRef, watch, defineComponent, toRaw } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { import {
Combobox as HCombobox, Combobox as HCombobox,
@@ -247,7 +247,7 @@ export default defineComponent({
}, },
searchablePlaceholder: { searchablePlaceholder: {
type: String, type: String,
default: 'Search...' default: () => configMenu.default.searchablePlaceholder.label
}, },
searchableLazy: { searchableLazy: {
type: Boolean, type: Boolean,
@@ -390,9 +390,9 @@ export default defineComponent({
} }
} else if (props.modelValue !== undefined && props.modelValue !== null) { } else if (props.modelValue !== undefined && props.modelValue !== null) {
if (props.valueAttribute) { if (props.valueAttribute) {
return selected.value?.[props.optionAttribute] ?? null return accessor(selected.value, props.optionAttribute) ?? null
} else { } else {
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : props.modelValue[props.optionAttribute] return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : accessor(props.modelValue as Record<string, any>, props.optionAttribute)
} }
} }
@@ -485,20 +485,30 @@ export default defineComponent({
lazy: props.searchableLazy lazy: props.searchableLazy
}) })
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, match => `\\${match}`)
}
function accessor<T extends Record<string, any>>(obj: T, key: string) {
return get(obj, key)
}
const filteredOptions = computed(() => { const filteredOptions = computed(() => {
if (!query.value || debouncedSearch) { if (!query.value || debouncedSearch) {
return options.value return options.value
} }
const escapedQuery = escapeRegExp(query.value)
return options.value.filter((option: any) => { return options.value.filter((option: any) => {
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => { return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => {
if (['string', 'number'].includes(typeof option)) { if (['string', 'number'].includes(typeof option)) {
return String(option).search(new RegExp(query.value, 'i')) !== -1 return String(option).search(new RegExp(escapedQuery, 'i')) !== -1
} }
const child = get(option, searchAttribute) const child = get(option, searchAttribute)
return child !== null && child !== undefined && String(child).search(new RegExp(query.value, 'i')) !== -1 return child !== null && child !== undefined && String(child).search(new RegExp(escapedQuery, 'i')) !== -1
}) })
}) })
}) })
@@ -511,7 +521,7 @@ export default defineComponent({
return null return null
} }
if (props.showCreateOptionWhen === 'always') { if (props.showCreateOptionWhen === 'always') {
const existingOption = filteredOptions.value.find(option => ['string', 'number'].includes(typeof option) ? option === query.value : option[props.optionAttribute] === query.value) const existingOption = filteredOptions.value.find(option => ['string', 'number'].includes(typeof option) ? option === query.value : accessor(option, props.optionAttribute) === query.value)
if (existingOption) { if (existingOption) {
return null return null
} }
@@ -541,6 +551,10 @@ export default defineComponent({
}) })
function onUpdate(value: any) { function onUpdate(value: any) {
if (toRaw(props.modelValue) === value) {
return
}
emit('update:modelValue', value) emit('update:modelValue', value)
emit('change', value) emit('change', value)
emitFormChange() emitFormChange()
@@ -567,6 +581,7 @@ export default defineComponent({
container, container,
selected, selected,
label, label,
accessor,
isLeading, isLeading,
isTrailing, isTrailing,
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys

View File

@@ -127,7 +127,7 @@ export default defineComponent({
default: () => ({}) default: () => ({})
}, },
modelModifiers: { modelModifiers: {
type: Object as PropType<{ trim?: boolean, lazy?: boolean, number?: boolean }>, type: Object as PropType<{ trim?: boolean, lazy?: boolean, number?: boolean, nullify?: boolean }>,
default: () => ({}) default: () => ({})
} }
}, },
@@ -137,7 +137,7 @@ export default defineComponent({
const { emitFormBlur, emitFormInput, inputId, color, size, name } = useFormGroup(props, config) const { emitFormBlur, emitFormInput, inputId, color, size, name } = useFormGroup(props, config)
const modelModifiers = ref(defu({}, props.modelModifiers, { trim: false, lazy: false, number: false })) const modelModifiers = ref(defu({}, props.modelModifiers, { trim: false, lazy: false, number: false, nullify: false }))
const textarea = ref<HTMLTextAreaElement | null>(null) const textarea = ref<HTMLTextAreaElement | null>(null)
@@ -158,10 +158,10 @@ export default defineComponent({
textarea.value.style.overflow = 'hidden' textarea.value.style.overflow = 'hidden'
const styles = window.getComputedStyle(textarea.value) const styles = window.getComputedStyle(textarea.value)
const paddingTop = parseInt(styles.paddingTop) const paddingTop = Number.parseInt(styles.paddingTop)
const paddingBottom = parseInt(styles.paddingBottom) const paddingBottom = Number.parseInt(styles.paddingBottom)
const padding = paddingTop + paddingBottom const padding = paddingTop + paddingBottom
const lineHeight = parseInt(styles.lineHeight) const lineHeight = Number.parseInt(styles.lineHeight)
const { scrollHeight } = textarea.value const { scrollHeight } = textarea.value
const newRows = (scrollHeight - padding) / lineHeight const newRows = (scrollHeight - padding) / lineHeight
@@ -183,6 +183,10 @@ export default defineComponent({
value = looseToNumber(value) value = looseToNumber(value)
} }
if (modelModifiers.value.nullify) {
value ||= null
}
emit('update:modelValue', value) emit('update:modelValue', value)
emitFormInput() emitFormInput()
} }

View File

@@ -66,7 +66,7 @@ export default defineComponent({
}, },
type: { type: {
type: String as PropType<'solid' | 'dotted' | 'dashed'>, type: String as PropType<'solid' | 'dotted' | 'dashed'>,
default: 'solid', default: () => config.default.type,
validator: (value: string) => ['solid', 'dotted', 'dashed'].includes(value) validator: (value: string) => ['solid', 'dotted', 'dashed'].includes(value)
}, },
class: { class: {

View File

@@ -72,10 +72,10 @@ import { twJoin } from 'tailwind-merge'
import { defu } from 'defu' import { defu } from 'defu'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import CommandPaletteGroup from './CommandPaletteGroup.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig } from '../../utils'
import type { Group, Command, Button, Strategy, DeepPartial } from '../../types/index' import type { Group, Command, Button, Strategy, DeepPartial } from '../../types/index'
import CommandPaletteGroup from './CommandPaletteGroup.vue'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { commandPalette } from '#ui/ui.config' import { commandPalette } from '#ui/ui.config'
@@ -269,13 +269,13 @@ export default defineComponent({
return getGroupWithCommands(group, commands) return getGroupWithCommands(group, commands)
}).filter(Boolean) }).filter(Boolean)
const searchGroups = props.groups.filter(group => !!group.search && searchResults.value[group.key]?.length).map(group => { const searchGroups = props.groups.filter(group => !!group.search && searchResults.value[group.key]?.length).map((group) => {
const commands = (searchResults.value[group.key] || []) const commands = (searchResults.value[group.key] || [])
return getGroupWithCommands(group, [...commands]) return getGroupWithCommands(group, [...commands])
}) })
const staticGroups: Group[] = props.groups.filter((group) => group.static && group.commands?.length).map((group) => { const staticGroups: Group[] = props.groups.filter(group => group.static && group.commands?.length).map((group) => {
return getGroupWithCommands(group, group.commands) return getGroupWithCommands(group, group.commands)
}) })

View File

@@ -77,7 +77,7 @@ import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue' import UKbd from '../elements/Kbd.vue'
import type { Command, Group } from '../../types/index' import type { Command, Group } from '../../types/index'
import { commandPalette } from '#ui/ui.config' import type { commandPalette } from '#ui/ui.config'
import { useId } from '#imports' import { useId } from '#imports'
export default defineComponent({ export default defineComponent({

View File

@@ -1,6 +1,6 @@
<template> <template>
<div :class="ui.wrapper" v-bind="attrs"> <div :class="ui.wrapper" v-bind="attrs">
<slot name="first" :on-click="onClickFirst"> <slot name="first" :on-click="onClickFirst" :can-go-first="canGoFirstOrPrev">
<UButton <UButton
v-if="firstButton && showFirst" v-if="firstButton && showFirst"
:size="size" :size="size"
@@ -14,7 +14,7 @@
/> />
</slot> </slot>
<slot name="prev" :on-click="onClickPrev"> <slot name="prev" :on-click="onClickPrev" :can-go-prev="canGoFirstOrPrev">
<UButton <UButton
v-if="prevButton" v-if="prevButton"
:size="size" :size="size"
@@ -41,7 +41,7 @@
@click="() => onClickPage(page)" @click="() => onClickPage(page)"
/> />
<slot name="next" :on-click="onClickNext"> <slot name="next" :on-click="onClickNext" :can-go-next="canGoLastOrNext">
<UButton <UButton
v-if="nextButton" v-if="nextButton"
:size="size" :size="size"
@@ -55,7 +55,7 @@
/> />
</slot> </slot>
<slot name="last" :on-click="onClickLast"> <slot name="last" :on-click="onClickLast" :can-go-last="canGoLastOrNext">
<UButton <UButton
v-if="lastButton && showLast" v-if="lastButton && showLast"
:size="size" :size="size"
@@ -74,11 +74,11 @@
<script lang="ts"> <script lang="ts">
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { RouteLocationRaw } from '#vue-router'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig } from '../../utils'
import type { Button, ButtonSize, DeepPartial, Strategy } from '../../types/index' import type { Button, ButtonSize, DeepPartial, Strategy } from '../../types/index'
import type { RouteLocationRaw } from '#vue-router'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { pagination, button } from '#ui/ui.config' import { pagination, button } from '#ui/ui.config'

View File

@@ -24,7 +24,7 @@
:disabled="item.disabled" :disabled="item.disabled"
as="template" as="template"
> >
<button :class="[ui.list.tab.base, ui.list.tab.background, ui.list.tab.height, ui.list.tab.padding, ui.list.tab.size, ui.list.tab.font, ui.list.tab.rounded, ui.list.tab.shadow, selected ? ui.list.tab.active : ui.list.tab.inactive]"> <button :aria-label="item.ariaLabel" :class="[ui.list.tab.base, ui.list.tab.background, ui.list.tab.height, ui.list.tab.padding, ui.list.tab.size, ui.list.tab.font, ui.list.tab.rounded, ui.list.tab.shadow, selected ? ui.list.tab.active : ui.list.tab.inactive]">
<slot <slot
name="icon" name="icon"
:item="item" :item="item"

View File

@@ -14,7 +14,7 @@
ui.background, ui.background,
ui.ring, ui.ring,
ui.shadow, ui.shadow,
fullscreen ? ui.fullscreen : [ui.width, ui.height, ui.rounded, ui.margin], fullscreen ? ui.fullscreen : [ui.width, ui.height, ui.rounded, ui.margin]
]" ]"
> >
<slot /> <slot />

View File

@@ -23,11 +23,11 @@
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import UNotification from './Notification.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useToast } from '../../composables/useToast' import { useToast } from '../../composables/useToast'
import { mergeConfig } from '../../utils' import { mergeConfig } from '../../utils'
import type { DeepPartial, Notification, Strategy } from '../../types/index' import type { DeepPartial, Notification, Strategy } from '../../types/index'
import UNotification from './Notification.vue'
import { useState } from '#imports' import { useState } from '#imports'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -181,7 +181,9 @@ export default defineComponent({
return return
} }
openTimeout = openTimeout || setTimeout(() => { openTimeout = openTimeout || setTimeout(() => {
popoverApi.value.togglePopover && popoverApi.value.togglePopover() if (popoverApi.value.togglePopover) {
popoverApi.value.togglePopover()
}
openTimeout = null openTimeout = null
}, props.openDelay) }, props.openDelay)
} }
@@ -201,7 +203,9 @@ export default defineComponent({
return return
} }
closeTimeout = closeTimeout || setTimeout(() => { closeTimeout = closeTimeout || setTimeout(() => {
popoverApi.value.closePopover && popoverApi.value.closePopover() if (popoverApi.value.closePopover) {
popoverApi.value.closePopover()
}
closeTimeout = null closeTimeout = null
}, props.closeDelay) }, props.closeDelay)
} }

View File

@@ -141,7 +141,6 @@ export default defineComponent({
} }
}) })
function close(value: boolean) { function close(value: boolean) {
if (props.preventClose) { if (props.preventClose) {
emit('close-prevented') emit('close-prevented')

View File

@@ -40,8 +40,6 @@ import type { DeepPartial, PopperOptions, Strategy } from '../../types/index'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { tooltip } from '#ui/ui.config' import { tooltip } from '#ui/ui.config'
// import useslots
const config = mergeConfig<typeof tooltip>(appConfig.ui.strategy, appConfig.ui.tooltip, tooltip) const config = mergeConfig<typeof tooltip>(appConfig.ui.strategy, appConfig.ui.tooltip, tooltip)

View File

@@ -32,7 +32,9 @@ interface Shortcut {
// keyCode?: number // keyCode?: number
} }
// eslint-disable-next-line regexp/no-super-linear-backtracking
const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/ const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/
// eslint-disable-next-line regexp/no-super-linear-backtracking
const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/ const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/
export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptions = {}) => { export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptions = {}) => {
@@ -48,9 +50,11 @@ export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptio
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
// Input autocomplete triggers a keydown event // Input autocomplete triggers a keydown event
if (!e.key) { return } if (!e.key) {
return
}
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key) const alphabeticalKey = /^[a-z]$/i.test(e.key)
let chainedKey let chainedKey
chainedInputs.value.push(e.key) chainedInputs.value.push(e.key)
@@ -59,7 +63,9 @@ export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptio
chainedKey = chainedInputs.value.slice(-2).join('-') chainedKey = chainedInputs.value.slice(-2).join('-')
for (const shortcut of shortcuts.filter(s => s.chained)) { for (const shortcut of shortcuts.filter(s => s.chained)) {
if (shortcut.key !== chainedKey) { continue } if (shortcut.key !== chainedKey) {
continue
}
if (shortcut.condition.value) { if (shortcut.condition.value) {
e.preventDefault() e.preventDefault()
@@ -72,12 +78,20 @@ export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptio
// try matching a standard shortcut // try matching a standard shortcut
for (const shortcut of shortcuts.filter(s => !s.chained)) { for (const shortcut of shortcuts.filter(s => !s.chained)) {
if (e.key.toLowerCase() !== shortcut.key) { continue } if (e.key.toLowerCase() !== shortcut.key) {
if (e.metaKey !== shortcut.metaKey) { continue } continue
if (e.ctrlKey !== shortcut.ctrlKey) { continue } }
if (e.metaKey !== shortcut.metaKey) {
continue
}
if (e.ctrlKey !== shortcut.ctrlKey) {
continue
}
// shift modifier is only checked in combination with alphabetical keys // shift modifier is only checked in combination with alphabetical keys
// (shift with non-alphabetical keys would change the key) // (shift with non-alphabetical keys would change the key)
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) { continue } if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) {
continue
}
// alt modifier changes the combined key anyways // alt modifier changes the combined key anyways
// if (e.altKey !== shortcut.altKey) { continue } // if (e.altKey !== shortcut.altKey) { continue }

View File

@@ -1,6 +1,6 @@
import { computed, ref, provide, inject, onMounted, onUnmounted, getCurrentInstance } from 'vue' import { computed, ref, provide, inject, onMounted, onUnmounted, getCurrentInstance } from 'vue'
import type { Ref, ComponentInternalInstance } from 'vue' import type { Ref, ComponentInternalInstance } from 'vue'
import { buttonGroup } from '#ui/ui.config' import type { buttonGroup } from '#ui/ui.config'
type ButtonGroupProps = { type ButtonGroupProps = {
orientation?: Ref<'horizontal' | 'vertical'> orientation?: Ref<'horizontal' | 'vertical'>

View File

@@ -11,7 +11,6 @@ type InputProps = {
legend?: string | null legend?: string | null
} }
export const useFormGroup = (inputProps?: InputProps, config?: any, bind: boolean = true) => { export const useFormGroup = (inputProps?: InputProps, config?: any, bind: boolean = true) => {
const formBus = inject<UseEventBusReturn<FormEvent, string> | undefined>('form-events', undefined) const formBus = inject<UseEventBusReturn<FormEvent, string> | undefined>('form-events', undefined)
const formGroup = inject<InjectedFormGroupValue | undefined>('form-group', undefined) const formGroup = inject<InjectedFormGroupValue | undefined>('form-group', undefined)

View File

@@ -36,15 +36,23 @@ export function usePopper ({
onMounted(() => { onMounted(() => {
watchEffect((onInvalidate) => { watchEffect((onInvalidate) => {
if (!popper.value) { return } if (!popper.value) {
if (!reference.value && !virtualReference?.value) { return } return
}
if (!reference.value && !virtualReference?.value) {
return
}
const popperEl = unrefElement(popper) const popperEl = unrefElement(popper)
const referenceEl = virtualReference?.value || unrefElement(reference) const referenceEl = virtualReference?.value || unrefElement(reference)
// if (!(referenceEl instanceof HTMLElement)) { return } // if (!(referenceEl instanceof HTMLElement)) { return }
if (!(popperEl instanceof HTMLElement)) { return } if (!(popperEl instanceof HTMLElement)) {
if (!referenceEl) { return } return
}
if (!referenceEl) {
return
}
const config: Record<string, any> = { const config: Record<string, any> = {
modifiers: [ modifiers: [

View File

@@ -1,8 +1,8 @@
import { computed, toValue, useAttrs } from 'vue' import { computed, toValue, useAttrs } from 'vue'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { useAppConfig } from '#imports'
import { mergeConfig, omit, get } from '../utils' import { mergeConfig, omit, get } from '../utils'
import type { DeepPartial, Strategy } from '../types/index' import type { DeepPartial, Strategy } from '../types/index'
import { useAppConfig } from '#imports'
export const useUI = <T>(key, $ui?: Ref<DeepPartial<T> & { strategy?: Strategy } | undefined>, $config?: Ref<T> | T, $wrapperClass?: Ref<string>, withAppConfig: boolean = false) => { export const useUI = <T>(key, $ui?: Ref<DeepPartial<T> & { strategy?: Strategy } | undefined>, $config?: Ref<T> | T, $wrapperClass?: Ref<string>, withAppConfig: boolean = false) => {
const $attrs = useAttrs() const $attrs = useAttrs()

View File

@@ -1,7 +1,7 @@
import { defineNuxtPlugin } from '#imports'
import { shallowRef } from 'vue' import { shallowRef } from 'vue'
import { modalInjectionKey } from '../composables/useModal' import { modalInjectionKey } from '../composables/useModal'
import type { ModalState } from '../types/modal' import type { ModalState } from '../types/modal'
import { defineNuxtPlugin } from '#imports'
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
const modalState = shallowRef<ModalState>({ const modalState = shallowRef<ModalState>({

View File

@@ -1,7 +1,7 @@
import { defineNuxtPlugin } from '#imports'
import { shallowRef } from 'vue' import { shallowRef } from 'vue'
import { slidOverInjectionKey } from '../composables/useSlideover' import { slidOverInjectionKey } from '../composables/useSlideover'
import type { SlideoverState } from '../types/slideover' import type { SlideoverState } from '../types/slideover'
import { defineNuxtPlugin } from '#imports'
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
const slideoverState = shallowRef<SlideoverState>({ const slideoverState = shallowRef<SlideoverState>({

View File

@@ -3,7 +3,7 @@ import type { Button } from './button'
export interface AccordionItem extends Button { export interface AccordionItem extends Button {
slot?: string slot?: string
disabled?: boolean disabled?: boolean
content?: string content?: string | string[] | object | object[]
defaultOpen?: boolean defaultOpen?: boolean
closeOthers?: boolean closeOthers?: boolean
} }

View File

@@ -1,8 +1,8 @@
import { alert } from '../ui.config'
import type { NestedKeyOf, ExtractDeepKey, ExtractDeepObject } from '.'
import type { Button } from './button'
import colors from '#ui-colors'
import type { AppConfig } from 'nuxt/schema' import type { AppConfig } from 'nuxt/schema'
import type { alert } from '../ui.config'
import type { Button } from './button'
import type { NestedKeyOf, ExtractDeepKey, ExtractDeepObject } from '.'
import type colors from '#ui-colors'
export type AlertColor = keyof typeof alert.color | ExtractDeepKey<AppConfig, ['ui', 'alert', 'color']> | typeof colors[number] export type AlertColor = keyof typeof alert.color | ExtractDeepKey<AppConfig, ['ui', 'alert', 'color']> | typeof colors[number]
export type AlertVariant = keyof typeof alert.variant | ExtractDeepKey<AppConfig, ['ui', 'alert', 'variant']> | NestedKeyOf<typeof alert.color> | NestedKeyOf<ExtractDeepObject<AppConfig, ['ui', 'alert', 'color']>> export type AlertVariant = keyof typeof alert.variant | ExtractDeepKey<AppConfig, ['ui', 'alert', 'variant']> | NestedKeyOf<typeof alert.color> | NestedKeyOf<ExtractDeepObject<AppConfig, ['ui', 'alert', 'color']>>

View File

@@ -1,7 +1,7 @@
import { avatar } from '../ui.config'
import type { ExtractDeepKey } from '.'
import colors from '#ui-colors'
import type { AppConfig } from 'nuxt/schema' import type { AppConfig } from 'nuxt/schema'
import type { avatar } from '../ui.config'
import type { ExtractDeepKey } from '.'
import type colors from '#ui-colors'
export type AvatarSize = keyof typeof avatar.size | ExtractDeepKey<AppConfig, ['ui', 'avatar', 'size']> export type AvatarSize = keyof typeof avatar.size | ExtractDeepKey<AppConfig, ['ui', 'avatar', 'size']>
export type AvatarChipColor = 'gray' | typeof colors[number] export type AvatarChipColor = 'gray' | typeof colors[number]

View File

@@ -1,7 +1,7 @@
import { badge } from '../ui.config'
import type { NestedKeyOf, ExtractDeepKey, ExtractDeepObject } from '.'
import colors from '#ui-colors'
import type { AppConfig } from 'nuxt/schema' import type { AppConfig } from 'nuxt/schema'
import type { badge } from '../ui.config'
import type { NestedKeyOf, ExtractDeepKey, ExtractDeepObject } from '.'
import type colors from '#ui-colors'
export type BadgeSize = keyof typeof badge.size | ExtractDeepKey<AppConfig, ['ui', 'badge', 'size']> export type BadgeSize = keyof typeof badge.size | ExtractDeepKey<AppConfig, ['ui', 'badge', 'size']>
export type BadgeColor = keyof typeof badge.color | ExtractDeepKey<AppConfig, ['ui', 'badge', 'color']> | typeof colors[number] export type BadgeColor = keyof typeof badge.color | ExtractDeepKey<AppConfig, ['ui', 'badge', 'color']> | typeof colors[number]

View File

@@ -1,8 +1,8 @@
import type { Link } from './link'
import { button } from '../ui.config'
import type { NestedKeyOf, ExtractDeepKey, ExtractDeepObject } from '.'
import colors from '#ui-colors'
import type { AppConfig } from 'nuxt/schema' import type { AppConfig } from 'nuxt/schema'
import type { button } from '../ui.config'
import type { Link } from './link'
import type { NestedKeyOf, ExtractDeepKey, ExtractDeepObject } from '.'
import type colors from '#ui-colors'
export type ButtonSize = keyof typeof button.size | ExtractDeepKey<AppConfig, ['ui', 'button', 'size']> export type ButtonSize = keyof typeof button.size | ExtractDeepKey<AppConfig, ['ui', 'button', 'size']>
export type ButtonColor = keyof typeof button.color | ExtractDeepKey<AppConfig, ['ui', 'button', 'color']> | typeof colors[number] export type ButtonColor = keyof typeof button.color | ExtractDeepKey<AppConfig, ['ui', 'button', 'color']> | typeof colors[number]
@@ -24,8 +24,7 @@ export interface Button extends Link {
trailingIcon?: string trailingIcon?: string
trailing?: boolean trailing?: boolean
leading?: boolean leading?: boolean
to?: string | object
target?: string
square?: boolean square?: boolean
truncate?: boolean truncate?: boolean
target?: string
} }

View File

@@ -1,5 +1,5 @@
import { chip } from '../ui.config' import type { chip } from '../ui.config'
import colors from '#ui-colors' import type colors from '#ui-colors'
export type ChipSize = keyof typeof chip.size export type ChipSize = keyof typeof chip.size
export type ChipColor = 'gray' | typeof colors[number] export type ChipColor = 'gray' | typeof colors[number]

View File

@@ -1,4 +1,4 @@
import { FuseResultMatch } from 'fuse.js' import type { FuseResultMatch } from 'fuse.js'
import type { Avatar } from './avatar' import type { Avatar } from './avatar'
export interface Command { export interface Command {
@@ -24,6 +24,6 @@ export interface Group {
commands?: Command[] commands?: Command[]
search?: (...args: any[]) => any[] | Promise<any[]> search?: (...args: any[]) => any[] | Promise<any[]>
filter?: (...args: any[]) => Command[] filter?: (...args: any[]) => Command[]
static?: Boolean static?: boolean
[key: string]: any [key: string]: any
} }

View File

@@ -5,10 +5,10 @@ T extends new () => { $props: infer P } ? NonNullable<P> :
export type ComponentSlots<T> = export type ComponentSlots<T> =
T extends new () => { $slots: infer S } ? NonNullable<S> : T extends new () => { $slots: infer S } ? NonNullable<S> :
T extends (props: any, ctx: { slots: infer S; attrs: any; emit: any }, ...args: any) => any ? NonNullable<S> : T extends (props: any, ctx: { slots: infer S, attrs: any, emit: any }, ...args: any) => any ? NonNullable<S> :
{} {}
export type ComponentEmit<T> = export type ComponentEmit<T> =
T extends new () => { $emit: infer E } ? NonNullable<E> : T extends new () => { $emit: infer E } ? NonNullable<E> :
T extends (props: any, ctx: { slots: any; attrs: any; emit: infer E }, ...args: any) => any ? NonNullable<E> : T extends (props: any, ctx: { slots: any, attrs: any, emit: infer E }, ...args: any) => any ? NonNullable<E> :
{} {}

View File

@@ -1,3 +1,3 @@
import { divider } from '#ui/ui.config' import type { divider } from '#ui/ui.config'
export type DividerSize = keyof typeof divider.border.size.horizontal | keyof typeof divider.border.size.vertical export type DividerSize = keyof typeof divider.border.size.horizontal | keyof typeof divider.border.size.vertical

View File

@@ -1,5 +1,5 @@
import type { NuxtLinkProps } from '#app'
import type { Avatar } from './avatar' import type { Avatar } from './avatar'
import type { NuxtLinkProps } from '#app'
export interface DropdownItem extends NuxtLinkProps { export interface DropdownItem extends NuxtLinkProps {
label: string label: string

View File

@@ -1,5 +1,5 @@
import { formGroup } from '../ui.config'
import type { ExtractDeepKey } from '.'
import type { AppConfig } from 'nuxt/schema' import type { AppConfig } from 'nuxt/schema'
import type { formGroup } from '../ui.config'
import type { ExtractDeepKey } from '.'
export type FormGroupSize = keyof typeof formGroup.size | ExtractDeepKey<AppConfig, ['ui', 'formGroup', 'size']> export type FormGroupSize = keyof typeof formGroup.size | ExtractDeepKey<AppConfig, ['ui', 'formGroup', 'size']>

View File

@@ -5,13 +5,18 @@ export interface FormError<T extends string = string> {
message: string message: string
} }
export interface ValidateReturnSchema<T> {
result: T
errors: FormError[]
}
export interface FormErrorWithId extends FormError { export interface FormErrorWithId extends FormError {
id: string id: string
} }
export interface Form<T> { export interface Form<T> {
validate(path?: string | string[], opts?: { silent?: true }): Promise<T | false>; validate(path?: string | string[], opts?: { silent?: true }): Promise<T | false>
validate(path?: string | string[], opts?: { silent?: false }): Promise<T>; validate(path?: string | string[], opts?: { silent?: false }): Promise<T>
clear(path?: string): void clear(path?: string): void
errors: Ref<FormError[]> errors: Ref<FormError[]>
setErrors(errs: FormError[], path?: string): void setErrors(errs: FormError[], path?: string): void

View File

@@ -23,6 +23,7 @@ export * from './popper'
export * from './progress' export * from './progress'
export * from './range' export * from './range'
export * from './select' export * from './select'
export * from './table'
export * from './tabs' export * from './tabs'
export * from './textarea' export * from './textarea'
export * from './toggle' export * from './toggle'

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