Compare commits

...

59 Commits

Author SHA1 Message Date
Benjamin Canac
6e4230fd63 chore(release): 2.12.0 2024-01-09 17:42:24 +01:00
Benjamin Canac
0be676a9ef docs: bump @nuxt/ui-pro-edge 2024-01-09 17:13:55 +01:00
Benjamin Canac
e48b61b5df docs(TableExampleAdvanced): improve sorting with v-model:sort and sort-mode props
Resolves #1177
2024-01-09 15:13:29 +01:00
Benjamin Canac
56e0c9a9a0 feat(Table): add sort-mode prop
Resolves #1149
2024-01-09 14:54:13 +01:00
Ivan Topolnjak
c6841d06a4 fix(Table): respect sort prop updates from parent component (#1208)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-01-09 13:03:02 +01:00
Benjamin Canac
8508e84958 chore(Table): rename columns sortFn to sort 2024-01-09 12:05:32 +01:00
Benjamin Canac
6154ae94a9 docs: bump @nuxt/ui-pro + @nuxt/image (required by BlogPost) 2024-01-08 17:29:19 +01:00
renovate[bot]
6384edf92a chore(deps): update all non-major dependencies (#1196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-01-08 14:54:03 +01:00
Ragura
4f3af6cfdb feat(Table): add custom sort function to columns (#1075)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-01-08 12:28:19 +01:00
Daniel Roe
893b2466ff docs: ignore /pro when no token is provided (#1205) 2024-01-06 13:36:33 +01:00
Benjamin Canac
9807e58f8f docs(installation): add Modules and TypeScript section
Related to #1169, #1200, #571 and #1190
2024-01-05 18:54:29 +01:00
Benjamin Canac
4124406032 fix(Card)!: remove overflow-hidden on wrapper
Fixes #806, fixes #1034
2024-01-05 17:37:35 +01:00
Benjamin Canac
3258167a14 fix(forms): dont disable inputs and selects on loading
Resolves #1117
2024-01-05 17:35:18 +01:00
Benjamin Canac
520624bd64 chore(InputMenu): handle async search with search prop 2024-01-05 17:33:36 +01:00
Benjamin Canac
e4b8fffc32 docs(SelectMenu): improve async search example 2024-01-05 17:29:47 +01:00
Benjamin Canac
5d781112f1 fix(Alert): always pass a function to actions click events
Fixes #1197
2024-01-04 11:05:33 +01:00
Benjamin Canac
1c8122a00b chore(deps): pin vue to 3.3.13 2024-01-04 10:53:08 +01:00
Benjamin Canac
0976833753 feat(Pagination): add disabled prop
Resolves #1189
2024-01-03 18:18:01 +01:00
Benjamin Canac
bc00f9c4b2 fix(Popover): improve placement with hover mode
Improvement of #781
2024-01-03 17:21:35 +01:00
Benjamin Canac
c6aa4215d7 fix(Dropdown): improve placement with hover mode
Resolves #1179
2024-01-03 17:20:06 +01:00
Benjamin Canac
3334e2af3d fix(Popover): allow manual mode without blocking normal behaviour 2024-01-03 16:51:20 +01:00
Benjamin Canac
3844714644 feat(Dropdown): handle manual mode
Resolves #1143
2024-01-03 16:49:00 +01:00
Benjamin Canac
84e6392981 chore(deps): update lock 2024-01-03 15:44:33 +01:00
Benjamin Canac
c2ef6237d8 chore(InputMenu): allow control of search query 2024-01-03 15:13:34 +01:00
Benjamin Canac
f735db04d6 feat(SelectMenu): allow control of search query
Resolves #1174
2024-01-03 15:13:34 +01:00
Benjamin Canac
e8f573b6bb fix(SelectMenu): input border focus after tailwindcss 3.4 2024-01-03 15:13:34 +01:00
renovate[bot]
288abf239f chore(deps): update all non-major dependencies (#1184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-03 14:54:34 +01:00
Daniel Roe
44d93a1cfd chore(Accordion): match event listener types (#1193) 2024-01-03 14:53:58 +01:00
Daniel Roe
217840bb41 docs: allow generating docs without github token (#1194) 2024-01-03 14:52:32 +01:00
Romain Hamel
ea2a24b5fe fix(Form): memory leak (#1185) 2024-01-02 16:36:35 +01:00
Romain Hamel
4a25a12390 feat(Form): expose submit function (#1186) 2024-01-02 16:28:38 +01:00
Sébastien Chopin
d64cb8a6fd chore: Update PULL_REQUEST_TEMPLATE.md 2024-01-02 14:57:32 +01:00
Benjamin Canac
00d0fd5919 fix(Table): display nothing instead of error when key is missing
Fixes #1173
2024-01-02 11:10:42 +01:00
Benjamin Canac
30e7a3ca20 chore(deps): update lock 2024-01-02 10:54:32 +01:00
renovate[bot]
7d572c81bb chore(deps): update all non-major dependencies (#1007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 10:37:51 +01:00
John Puaoi
97a3975197 fix(Form): invalid errors when using clear by path (#1165) 2024-01-02 10:34:52 +01:00
Victor Akintunde
43b999c88e docs(notification): add default timeout value (#1181) 2024-01-02 10:33:08 +01:00
Benjamin Canac
7151b7b97d fix(Dropdown): merge item class
Fixes #1157
2023-12-28 13:14:17 +01:00
Conner Blanton
ffd20b3991 feat(VerticalNavigation): ability to add dividers (#963)
* feat(VerticalNavigation): ability to add sections with divider

* lint fix

* updating branch. resolving conflicts

* reverting app.vue

* removing unnecessary style

---------

Co-authored-by: Inesh Bose <dev@inesh.xyz>
2023-12-27 14:34:51 +01:00
Neil Mispelaar
29e64ca963 feat(VerticalNavigation): improve accessibility (#948)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-12-26 13:00:33 +01:00
Benjamin Canac
556ee0d9c4 docs: update dependencies 2023-12-26 12:19:24 +01:00
Benjamin Canac
debafef0fa chore(deps): update dependencies 2023-12-26 11:15:44 +01:00
Benjamin Canac
2d9038bcb0 chore(npmrc): ignore workspace root check 2023-12-26 11:06:36 +01:00
nathanjcollins
f7f8f06b91 chore(deps): update tailwindcss dependency to 3.4.0 (#1145)
* chore: update dependency for tailwindcss to 3.4

* refactor: change w-x h-x to size-x

* up

* up

* revert: h-x w-x to size-x

* fix: undo

* up

---------

Co-authored-by: Nathan Collins <nathancollins@Nathans-MacBook-Pro.local>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-12-26 10:56:57 +01:00
Benjamin Canac
56e1fed373 fix(useShortcuts): invalid code after #1159 2023-12-25 17:39:01 +01:00
Lukas
648eec31b9 fix(useShortcuts): include contenteditable="plaintext-only" elements in usingInput (#1159) 2023-12-25 17:34:24 +01:00
Benjamin Canac
d0ce8ee1c4 Revert "docs: pull nuxt/ui-pro docs from main branch"
This reverts commit 9d8f358139.
2023-12-21 16:19:31 +01:00
Benjamin Canac
9d8f358139 docs: pull nuxt/ui-pro docs from main branch 2023-12-21 11:18:14 +01:00
Jakub Chrobak
bc6474a9ad docs(installation): add bun command (#1141)
* docs: add bun installation command

* fix: add missing bracket
2023-12-20 12:28:14 +01:00
Benjamin Canac
31924e32f2 chore(deps): update 2023-12-19 18:16:39 +01:00
Benjamin Canac
c963ba688f docs(theming): improve Icons section 2023-12-19 17:51:27 +01:00
Benjamin Canac
4dee128524 chore(deps): update @egoist/tailwindcss-icons 2023-12-19 17:36:13 +01:00
Benjamin Canac
4c84839a01 fix(Toggle): add missing change event
Fixes #1113
2023-12-19 13:09:11 +01:00
renovate[bot]
fd30022550 chore(deps): update devdependency date-fns to v3 (#1127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-19 11:40:59 +01:00
Daniel Roe
1a1c640220 chore: migrate to @nuxt/test-utils alpha (#1133)
* test: update snapshots to remove quotes

* chore: add alpha versions of test-utils

* chore: migrate to new test format

* test: slightly improve typing in suite

* test: improve safety of basic test
2023-12-19 11:37:57 +01:00
Daniel Roe
5c99ae131d fix: update vue and fix type issues (#1112)
* chore: bump vue version to show type errors

* fix: address type issues
2023-12-19 10:54:00 +01:00
Benjamin Canac
b22bd70d54 fix(config): prevent class merge of avatar size 2023-12-16 00:16:34 +01:00
Benjamin Canac
0c8ab9d98e fix(RadioGroup): pass option.disabled to children
Fixes #1109
2023-12-16 00:16:34 +01:00
Inesh Bose
0fdc8f70b6 feat(SelectMenu): allow creating option despite search (#1080)
* chore: initial

* chore: use reusable vnode

* fix: use component with vnode

* chore: option placement

* chore: finish

* up

* up

* up

* fix(selectmenu): non-object custom options

* up

---------

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-12-15 15:04:06 +01:00
62 changed files with 3772 additions and 2666 deletions

View File

@@ -4,7 +4,7 @@
### 🔗 Linked issue
<!-- Please ensure there is an open issue and mention its number as #123 -->
<!-- Please ensure there is an open issue and mention its number as: Fixes #123 -->
### ❓ Type of change

1
.npmrc
View File

@@ -1,2 +1,3 @@
shamefully-hoist=true
auto-install-peers=true
ignore-workspace-root-check=true

View File

@@ -1,5 +1,58 @@
# Changelog
## [2.12.0](https://github.com/nuxt/ui/compare/v2.11.1...v2.12.0) (2024-01-09)
### ⚠ BREAKING CHANGES
* **Card:** remove `overflow-hidden` on wrapper
### Features
* **Breadcrumb:** handle `labelClass` and merge `iconClass` ([f623ec1](https://github.com/nuxt/ui/commit/f623ec1130edf448988784b36c15a850470685c4))
* **Dropdown:** handle `labelClass` and merge `iconClass` ([1c9835d](https://github.com/nuxt/ui/commit/1c9835d7f149231cf2e3e053e5ea08eceeaaa61d)), closes [#716](https://github.com/nuxt/ui/issues/716)
* **Dropdown:** handle manual mode ([3844714](https://github.com/nuxt/ui/commit/38447146445618a1310a6315c608f4cd21069e17)), closes [#1143](https://github.com/nuxt/ui/issues/1143)
* **Form:** expose submit function ([#1186](https://github.com/nuxt/ui/issues/1186)) ([4a25a12](https://github.com/nuxt/ui/commit/4a25a12390f8ecae83c1081c89eba99a8fda14f8))
* **InputMenu:** new component ([#1095](https://github.com/nuxt/ui/issues/1095)) ([6d8d82a](https://github.com/nuxt/ui/commit/6d8d82a265692aaee556e40b09e4b3048ae044da))
* **Pagination:** add `disabled` prop ([0976833](https://github.com/nuxt/ui/commit/0976833753cd2140649bc324f53a263d4e09ecff)), closes [#1189](https://github.com/nuxt/ui/issues/1189)
* **Popover:** open and close events ([#1038](https://github.com/nuxt/ui/issues/1038)) ([f32f578](https://github.com/nuxt/ui/commit/f32f578125c12b35e59db2f7981c8b1b5a146397))
* **SelectMenu:** add `empty` slot when no options ([5d1919a](https://github.com/nuxt/ui/commit/5d1919a5381b316637d50405d287428f67f2b9cc)), closes [#1089](https://github.com/nuxt/ui/issues/1089)
* **SelectMenu:** allow control of search query ([f735db0](https://github.com/nuxt/ui/commit/f735db04d62fca678ca30ecd565b32e70bcda3e0)), closes [#1174](https://github.com/nuxt/ui/issues/1174)
* **SelectMenu:** allow creating option despite search ([#1080](https://github.com/nuxt/ui/issues/1080)) ([0fdc8f7](https://github.com/nuxt/ui/commit/0fdc8f70b6a656114d30b07d682e4edcd61a23fb))
* **Table:** add `sort-mode` prop ([56e0c9a](https://github.com/nuxt/ui/commit/56e0c9a9a05e1e8491e2d460b8d51084bd2c1305)), closes [#1149](https://github.com/nuxt/ui/issues/1149)
* **Table:** add custom sort function to columns ([#1075](https://github.com/nuxt/ui/issues/1075)) ([4f3af6c](https://github.com/nuxt/ui/commit/4f3af6cfdb5213d1be3d2680fcf3a95f7b3bc0b3))
* **VerticalNavigation:** ability to add dividers ([#963](https://github.com/nuxt/ui/issues/963)) ([ffd20b3](https://github.com/nuxt/ui/commit/ffd20b3991a35ae7fa0e249fa009e330fd963705))
* **VerticalNavigation:** handle `labelClass` and merge `iconClass` ([a79f7c0](https://github.com/nuxt/ui/commit/a79f7c0a34c0414fe4feb95691e1f044b07ef087))
* **VerticalNavigation:** improve accessibility ([#948](https://github.com/nuxt/ui/issues/948)) ([29e64ca](https://github.com/nuxt/ui/commit/29e64ca963eeed1e82640957860f43391d8683ed))
### Bug Fixes
* **Alert:** always pass a function to actions click events ([5d78111](https://github.com/nuxt/ui/commit/5d781112f1eb464658c83047bf80c2ea7c9a2b05)), closes [#1197](https://github.com/nuxt/ui/issues/1197)
* **Card:** remove `overflow-hidden` on wrapper ([4124406](https://github.com/nuxt/ui/commit/412440603206151d63b04ffe6bed1bbc5b0e6615)), closes [#806](https://github.com/nuxt/ui/issues/806) [#1034](https://github.com/nuxt/ui/issues/1034)
* **config:** prevent class merge of `avatar` size ([b22bd70](https://github.com/nuxt/ui/commit/b22bd70d54e68c3217ba42690210084749fee656))
* **Dropdown:** improve placement with `hover` mode ([c6aa421](https://github.com/nuxt/ui/commit/c6aa4215d7f9003adeefa7cdff76c7a88715f20c)), closes [#1179](https://github.com/nuxt/ui/issues/1179)
* **Dropdown:** merge item `class` ([7151b7b](https://github.com/nuxt/ui/commit/7151b7b97d42f389506521044ebaffa8a299e7fb)), closes [#1157](https://github.com/nuxt/ui/issues/1157)
* **Form:** invalid errors when using `clear` by path ([#1165](https://github.com/nuxt/ui/issues/1165)) ([97a3975](https://github.com/nuxt/ui/commit/97a39751977bf1e942e2bafd5839141383b7af2f))
* **Form:** memory leak ([#1185](https://github.com/nuxt/ui/issues/1185)) ([ea2a24b](https://github.com/nuxt/ui/commit/ea2a24b5fe6ddc87e6eb951a662ce8b84b9d987f))
* **forms:** dont disable inputs and selects on `loading` ([3258167](https://github.com/nuxt/ui/commit/3258167a1431b664cd1dcc925a4b3fe06a996831)), closes [#1117](https://github.com/nuxt/ui/issues/1117)
* **Link:** handle `active` override when value is false ([83631cc](https://github.com/nuxt/ui/commit/83631ccbca1364f012b0c2899f97e2166dd1d360))
* **Popover:** allow manual mode without blocking normal behaviour ([3334e2a](https://github.com/nuxt/ui/commit/3334e2af3de2844de08ee530e62f2e4e2fd7ed24))
* **Popover:** improve placement with `hover` mode ([bc00f9c](https://github.com/nuxt/ui/commit/bc00f9c4b25dd4b99cb6e53014624f41ee929654)), closes [#781](https://github.com/nuxt/ui/issues/781)
* **RadioGroup:** pass `option.disabled` to children ([0c8ab9d](https://github.com/nuxt/ui/commit/0c8ab9d98e494c49cceac111edc0606ee4d63638)), closes [#1109](https://github.com/nuxt/ui/issues/1109)
* **SelectMenu:** input border focus after `tailwindcss` 3.4 ([e8f573b](https://github.com/nuxt/ui/commit/e8f573b6bb32a22873d9f93b40883ca12b481d7e))
* **Table:** display nothing instead of error when key is missing ([00d0fd5](https://github.com/nuxt/ui/commit/00d0fd59192cc171abb3d2ddaee46b2b9fa9422f)), closes [#1173](https://github.com/nuxt/ui/issues/1173)
* **Table:** respect sort prop updates from parent component ([#1208](https://github.com/nuxt/ui/issues/1208)) ([c6841d0](https://github.com/nuxt/ui/commit/c6841d06a48ffef95d238f94a4822a1e48b85422))
* **Toggle:** add missing `change` event ([4c84839](https://github.com/nuxt/ui/commit/4c84839a0183756b9f8df8674aace8cd40e44dcd)), closes [#1113](https://github.com/nuxt/ui/issues/1113)
* update vue and fix type issues ([#1112](https://github.com/nuxt/ui/issues/1112)) ([5c99ae1](https://github.com/nuxt/ui/commit/5c99ae131d1a50a8db21f1d5794a06080c515831))
* **useShortcuts:** include `contenteditable="plaintext-only"` elements in `usingInput` ([#1159](https://github.com/nuxt/ui/issues/1159)) ([648eec3](https://github.com/nuxt/ui/commit/648eec31b99fcffb65c042e0a5587da941c8e90f))
* **useShortcuts:** invalid code after [#1159](https://github.com/nuxt/ui/issues/1159) ([56e1fed](https://github.com/nuxt/ui/commit/56e1fed373786fc158ca9da9f02a9ec4e273afce))
### Reverts
* Revert "docs: pull `nuxt/ui-pro` docs from `main` branch" ([d0ce8ee](https://github.com/nuxt/ui/commit/d0ce8ee1c4a3d7b2285885d76e02e03168011110))
## [2.11.1](https://github.com/nuxt/ui/compare/v2.11.0...v2.11.1) (2023-12-11)

View File

@@ -0,0 +1,22 @@
<script setup>
const items = [
[{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
}]
]
const open = ref(true)
defineShortcuts({
o: () => open.value = !open.value
})
</script>
<template>
<UDropdown v-model:open="open" :items="items" :popper="{ placement: 'bottom-start' }">
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
</UDropdown>
</template>

View File

@@ -0,0 +1,26 @@
<script setup>
const loading = ref(false)
const selected = ref()
async function search (q) {
loading.value = true
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
loading.value = false
return users
}
</script>
<template>
<UInputMenu
v-model="selected"
:search="search"
:loading="loading"
placeholder="Search for a user..."
option-attribute="name"
trailing
by="id"
/>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref()
const query = ref('Wade')
</script>
<template>
<UInputMenu
v-model="selected"
v-model:query="query"
:options="people"
placeholder="Select a person"
/>
</template>

View File

@@ -1,18 +1,19 @@
<script setup>
const open = ref(false)
const open = ref(true)
defineShortcuts({
o: () => open.value = !open.value
})
</script>
<template>
<div class="flex gap-4 items-center">
<UToggle v-model="open" />
<UPopover :open="open">
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
<UPopover v-model:open="open">
<UButton color="white" :label="open.toString()" trailing-icon="i-heroicons-chevron-down-20-solid" />
<template #panel>
<div class="p-4">
<Placeholder class="h-20 w-48" />
</div>
</template>
</UPopover>
</div>
<template #panel>
<div class="p-4">
<Placeholder class="h-20 w-48" />
</div>
</template>
</UPopover>
</template>

View File

@@ -1,13 +1,11 @@
<template>
<div class="flex gap-4 items-center">
<UPopover overlay>
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
<UPopover overlay>
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
<template #panel>
<div class="p-4">
<Placeholder class="h-20 w-48" />
</div>
</template>
</UPopover>
</div>
<template #panel>
<div class="p-4">
<Placeholder class="h-20 w-48" />
</div>
</template>
</UPopover>
</template>

View File

@@ -23,6 +23,7 @@ const labels = computed({
// In a real app, you would make an API call to create the label
const response = {
id: options.value.length + 1,
name: label.name,
color: generateColorFromString(label.name)
}

View File

@@ -0,0 +1,53 @@
<script setup>
const options = ref([
{ id: 1, name: 'bug' },
{ id: 2, name: 'documentation' },
{ id: 3, name: 'duplicate' },
{ id: 4, name: 'enhancement' },
{ id: 5, name: 'good first issue' },
{ id: 6, name: 'help wanted' },
{ id: 7, name: 'invalid' },
{ id: 8, name: 'question' },
{ id: 9, name: 'wontfix' }
])
const selected = ref([])
const labels = computed({
get: () => selected.value,
set: async (labels) => {
const promises = labels.map(async (label) => {
if (label.id) {
return label
}
// In a real app, you would make an API call to create the label
const response = {
id: options.value.length + 1,
name: label.name
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
multiple
searchable
creatable
show-create-option-when="always"
placeholder="Select labels"
/>
</template>

View File

@@ -1,19 +1,27 @@
<script setup>
const search = async (q) => {
const loading = ref(false)
const selected = ref([])
async function search (q) {
loading.value = true
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email })).filter(Boolean)
}
loading.value = false
const selected = ref([])
return users
}
</script>
<template>
<USelectMenu
v-model="selected"
:loading="loading"
:searchable="search"
placeholder="Search for a user..."
option-attribute="name"
multiple
trailing
by="id"
/>
</template>

View File

@@ -0,0 +1,16 @@
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref()
const query = ref('Wade')
</script>
<template>
<USelectMenu
v-model="selected"
v-model:query="query"
:options="people"
placeholder="Select a person"
searchable
/>
</template>

View File

@@ -77,6 +77,7 @@ const resetFilters = () => {
}
// Pagination
const sort = ref({ column: 'id', direction: 'asc' as const })
const page = ref(1)
const pageCount = ref(10)
const pageTotal = ref(200) // This value should be dynamic coming from the API
@@ -92,11 +93,13 @@ const { data: todos, pending } = await useLazyAsyncData<{
query: {
q: search.value,
'_page': page.value,
'_limit': pageCount.value
'_limit': pageCount.value,
'_sort': sort.value.column,
'_order': sort.value.direction
}
}), {
default: () => [],
watch: [page, search, searchStatus, pageCount]
watch: [page, search, searchStatus, pageCount, sort]
})
</script>
@@ -175,11 +178,13 @@ const { data: todos, pending } = await useLazyAsyncData<{
<!-- Table -->
<UTable
v-model="selectedRows"
v-model:sort="sort"
:rows="todos"
:columns="columnsTable"
:loading="pending"
sort-asc-icon="i-heroicons-arrow-up"
sort-desc-icon="i-heroicons-arrow-down"
sort-mode="manual"
class="w-full"
:ui="{ td: { base: 'max-w-[0] truncate' } }"
@select="select"

View File

@@ -60,5 +60,5 @@ const people = [{
</script>
<template>
<UTable :columns="columns" :rows="people" :sort="{ column: 'title' }" />
<UTable :columns="columns" :rows="people" />
</template>

View File

@@ -0,0 +1,41 @@
<script setup>
const links = [
[
{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
},
badge: 100
}, {
label: 'Installation',
icon: 'i-heroicons-home',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
icon: 'i-heroicons-chart-bar',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
icon: 'i-heroicons-command-line',
to: '/navigation/command-palette'
}
],
[
{
label: 'Examples',
icon: 'i-heroicons-light-bulb',
to: '/getting-started/examples#verticalnavigation'
},
{
label: 'Help',
icon: 'i-heroicons-question-mark-circle',
to: '/getting-started/examples'
}
]
]
</script>
<template>
<UVerticalNavigation :links="links" />
</template>

View File

@@ -20,6 +20,10 @@ yarn add @nuxt/ui
npm install @nuxt/ui
```
```bash [bun]
bun add @nuxt/ui
```
::
2. Add it to your `modules` section in your `nuxt.config`:
@@ -32,8 +36,114 @@ export default defineNuxtConfig({
That's it! You can now use all the components and composables in your Nuxt app ✨
## Modules
Nuxt UI will automatically install the [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/), [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) and [nuxt-icon](https://github.com/nuxt-modules/icon) modules for you.
::callout{icon="i-heroicons-exclamation-triangle"}
As this module installs [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/) and [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) for you, you should remove them from your `modules` and `dependencies` if you've previously installed them manually.
You should remove them from your `modules` and `dependencies` if you've previously installed them.
::
### `@nuxtjs/tailwindcs`
This module is pre-configured and will automatically load the following plugins:
- [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms)
- [@tailwindcss/typography](https://github.com/tailwindlabs/tailwindcss-typography)
- [@tailwindcss/aspect-ratio](https://github.com/tailwindlabs/tailwindcss-aspect-ratio)
- [@tailwindcss/container-queries](https://github.com/tailwindlabs/tailwindcss-container-queries)
- [@headlessui/tailwindcss](https://github.com/tailwindlabs/headlessui/tree/main/packages/%40headlessui-vue)
Note that the `@tailwindcss/aspect-ratio` plugin disables the default aspect ratio utilities:
- `aspect-auto`
- `aspect-square`
- `aspect-video`
You can re-enable them by adding the following to your `tailwind.config.ts`:
```ts [tailwind.config.ts]
import type { Config } from 'tailwindcss'
export default <Partial<Config>>{
theme: {
extend: {
aspectRatio: {
auto: 'auto',
square: '1 / 1',
video: '16 / 9'
}
}
}
}
```
### `@nuxtjs/color-mode`
This module is installed to provide dark mode support out of the box thanks to the Tailwind CSS dark mode `class` strategy.
::callout{icon="i-heroicons-light-bulb"}
You can read more about this in the [Theming](/getting-started/theming#dark-mode) section.
::
### `nuxt-icon`
This module is installed when using the `dynamic` prop on the `Icon` component or globally through the `ui.icons.dynamic` option in your `app.config.ts`.
::callout{icon="i-heroicons-light-bulb"}
You can read more about this in the [Theming](/getting-started/theming#dynamic-icons) section and on the [Icon](/elements/icon) component page.
::
## TypeScript
This module is written in TypeScript and provides typings for all the components and composables. You can look at the [source code](https://github.com/nuxt/ui/tree/dev/src/runtime/types) to see all the available types.
::callout{icon="i-heroicons-light-bulb" to="https://nuxt.com/docs/guide/concepts/typescript" target="_blank"}
You can read more about TypeScript on the official Nuxt documentation.
::
You can use those types in your own components by importing them from `#ui/types`, for example when defining wrapper components:
```vue
<template>
<UBreadcrumb :links="links">
<template #icon="{ link }">
<UIcon :name="link.icon" />
</template>
</UBreadcrumb>
</template>
<script setup lang="ts">
import type { BreadcrumbLink } from '#ui/types'
export interface Props {
links: BreadcrumbLink[]
}
defineProps<Props>()
</script>
```
You don't have to use TypeScript yourself, but doing so will give you access to prop validation and autocomplete.
We've managed to provide dynamic typings on props such as `color`, `size`, `variant`, etc. based on your custom config. For example, you'll be suggested the `custom` color and the `subtle` variant when using the `Button` component with an `app.config.ts` as such:
```ts [app.config.ts]
export default defineAppConfig({
ui: {
button: {
color: {
custom: {
subtle: '...'
}
}
}
}
})
```
::callout{icon="i-heroicons-light-bulb"}
You can read more about components configuration in the [Theming](/getting-started/theming#appconfigts) section.
::
## IntelliSense

View File

@@ -281,12 +281,14 @@ export default defineNuxtConfig({
Search the icon you want to use on https://icones.js.org built by [@antfu](https://github.com/antfu).
::
Unlike the official [nuxt-icon](https://github.com/nuxt-modules/icon/) module, this module will not fetch any icon from the web and will only bundle the icons you use in your app thanks to [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons).
However, you will need to install either `@iconify/json` (full icon collections, 50MB) or the individual icon packages you want to use in your app.
Thanks to [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons) plugin, only the icons you use in your app will be bundled in your CSS. However, you need to install the icon collections you specified in the `ui.icons` key:
::code-group
```bash [pnpm]
pnpm i @iconify-json/{collection_name}
```
```bash [yarn]
yarn add @iconify-json/{collection_name}
```
@@ -295,25 +297,21 @@ yarn add @iconify-json/{collection_name}
npm install @iconify-json/{collection_name}
```
```sh [pnpm]
pnpm i @iconify-json/{collection_name}
```
::
When using `@iconify/json`, you can specifiy `icons: 'all'` in your `nuxt.config.ts` to use any icon in your app.
If you choose to use the full `@iconify/json` icon collection (50MB), you can specifiy `icons: 'all'` or `icons: {}` in your `nuxt.config.ts` to use any icon in your app.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
ui: {
icons: 'all'
icons: {}
}
})
```
### Custom config
If you have specific needs, like using a custom icon collection, you can use the `icons` option in your `nuxt.config.ts` as an object to override the config of the [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
If you have specific needs, like using a custom icon collection, you can use the `icons` option in your `nuxt.config.ts` as an object to override the config of the [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
```ts [nuxt.config.ts]
import { getIconCollections } from '@egoist/tailwindcss-icons'
@@ -345,7 +343,13 @@ export default defineNuxtConfig({
})
```
---
### Dynamic icons
The `Icon` component also has a `dynamic` prop to use the [nuxt-icon](https://github.com/nuxt-modules/icon/) module instead of the [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
Read more about this in the [Icon](/elements/icon#dynamic) component page.
### Defaults
You can easily replace all the default icons of the components in your `app.config.ts`.

View File

@@ -21,6 +21,7 @@ Pass an array of arrays to the `items` prop of the Dropdown component. Each arra
- `shortcuts` - The shortcuts of the item.
- `slot` - The slot of the item.
- `disabled` - Whether the item is disabled.
- `class` - The class of the item.
- `click` - The click handler of the item.
You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
@@ -33,6 +34,12 @@ Use the `mode` prop to switch between `click` and `hover` modes.
:component-example{component="dropdown-example-mode"}
### Manual :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Use a `v-model:open` to manually control the state. In this example, press :shortcut{value="O"} to toggle the dropdown.
:component-example{component="dropdown-example-open"}
## Popper
Use the `popper` prop to customize the popper instance.

View File

@@ -203,6 +203,9 @@ componentProps:
## API
::field-group
::field{name="submit ()" type="Promise<void>"}
Triggers form submission.
::
::field{name="validate (path?: string, opts: { silent?: boolean })" type="Promise<T>"}
Triggers form validation. Will raise any errors unless `opts.silent` is set to true.
::
@@ -217,5 +220,5 @@ componentProps:
::
::field{name="errors" type="Ref<FormError[]>"}
A reference to the array containing validation errors. Use this to access or manipulate the error information.
::
::
::

View File

@@ -100,6 +100,46 @@ excludedProps:
Learn how to customize icons from the [Input](/forms/input#icon) component.
::
## Searchable
### Attributes
Use the `search-attributes` prop with an array of property names to search on each option object. Nested attributes can be accessed using `dot.notation`. When the property value is an array or object, these are cast to string so these can be searched within.
::component-example
---
component: 'input-menu-example-search-attributes'
componentProps:
class: 'w-full lg:w-48'
---
::
### Control the query
Use a `v-model:query` to control the search query.
::component-example
---
component: 'input-menu-example-search-query'
componentProps:
class: 'w-full lg:w-48'
---
::
### Async search
Pass a function to the `search` prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.
Use the `debounce` prop to adjust the delay of the function.
::component-example
---
component: 'input-menu-example-search-async'
componentProps:
class: 'w-full lg:w-48'
---
::
## Popper
Use the `popper` prop to customize the popper instance.

View File

@@ -117,6 +117,8 @@ componentProps:
By default, the search query will be kept after the menu is closed. To clear it on close, set the `clear-search-on-close` prop.
You can also configure this globally through the `ui.selectMenu.default.clearSearchOnClose` config. Defaults to `false`.
::component-card
---
baseProps:
@@ -130,6 +132,18 @@ props:
---
::
### Control the query :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Use a `v-model:query` to control the search query.
::component-example
---
component: 'select-menu-example-search-query'
componentProps:
class: 'w-full lg:w-48'
---
::
### Async search
Pass a function to the `searchable` prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.
@@ -138,7 +152,7 @@ Use the `debounce` prop to adjust the delay of the function.
::component-example
---
component: 'select-menu-example-async-search'
component: 'select-menu-example-search-async'
componentProps:
class: 'w-full lg:w-48'
---
@@ -158,6 +172,20 @@ componentProps:
---
::
However, if you want to create options despite search query (apart from exact match), you can set the `show-create-option-when` prop to `'always'`.
You can also configure this globally through the `ui.selectMenu.default.showCreateOptionWhen` config. Defaults to `empty`.
Try to search for something that exists in the example below, but not an exact match.
::component-example
---
component: 'select-menu-example-creatable-always'
componentProps:
class: 'w-full lg:w-48'
---
::
## Popper
Use the `popper` prop to customize the popper instance.

View File

@@ -60,7 +60,7 @@ Use the `disabled` prop to disable the RadioGroup.
::component-card
---
baseProps:
options: [{ value: 'email', label: 'Email' }, { value: 'sms', label: 'Phone (SMS)' }, { value: 'push', label: 'Push notification' }]
options: [{ value: 'email', label: 'Email' }, { value: 'sms', label: 'Phone (SMS)' }, { value: 'push', label: 'Push notification', disabled: true }]
modelValue: 'sms'
props:
disabled: true
@@ -68,7 +68,7 @@ props:
::
::callout{icon="i-heroicons-light-bulb"}
This prop also work on the Radio component.
This prop also work on the Radio component and you can set the `disabled` field in the `options` to disable a specific Radio.
::
### Label

View File

@@ -6,6 +6,10 @@ links:
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/data/Table.vue
---
::callout{icon="i-heroicons-puzzle-piece" to="/getting-started/examples#table"}
Check out an example of a Table with advanced features like sorting, pagination, search, etc.
::
## Usage
Use the `rows` prop to set the data to display in the table. By default, the table will display all the fields of the rows.
@@ -28,6 +32,7 @@ Use the `columns` prop to configure which columns to display. It's an array of o
- `sortable` - Whether the column is sortable. Defaults to `false`.
- `direction` - The sort direction to use on first click. Defaults to `asc`.
- `class` - The class to apply to the column cells.
- `sort` - Pass your own `sort` function. Defaults to a simple _greater than_ / _less than_ comparison.
::component-example{class="grid"}
---
@@ -53,6 +58,8 @@ componentProps:
You can make the columns sortable by setting the `sortable` property to `true` in the column configuration.
You may specify the default direction of each column through the `direction` property. It can be either `asc` or `desc`, but it will default to `asc`.
::component-example{class="grid"}
---
padding: false
@@ -62,17 +69,84 @@ componentProps:
---
::
You may specify the default direction of each column through the `direction` property. It can be either `asc` or `desc`, but it will default to `asc`.
#### Default sorting
You can specify a default sort for the table through the `sort` prop. It's an object with the following properties:
- `column` - The column to sort by.
- `direction` - The sort direction. Can be either `asc` or `desc` and defaults to `asc`.
::callout{icon="i-heroicons-light-bulb"}
This will set the default sort and will work even if no column is set as `sortable`.
```vue
<script setup>
const sort = ref({
column: 'name',
direction: 'desc'
})
const columns = [{
label: 'Name',
key: 'name',
sortable: true
}]
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'
}]
</script>
<template>
<UTable :sort="sort" :columns="columns" :rows="people" />
</template>
```
#### Reactive sorting
You can use a `v-model:sort` to make the sorting reactive. You may also use `@update:sort` to call your own function with the sorting data.
When fetching data from an API, we can take advantage of the [`useFetch`](https://nuxt.com/docs/api/composables/use-fetch) or [`useAsyncData`](https://nuxt.com/docs/api/composables/use-async-data) composables to fetch the data based on the sorting column and direction every time the `sort` reactive element changes.
When doing so, you might want to set the `sort-mode` prop to `manual` to disable the automatic sorting and return the rows as is. :u-badge{label="New" class="!rounded-full" variant="subtle"}
```vue
<script setup>
// Ensure it uses `ref` instead of `reactive`.
const sort = ref({
column: 'name',
direction: 'desc'
})
const columns = [{
label: 'Name',
key: 'name',
sortable: true
}]
const { data, pending } = await useLazyFetch(() => `/api/users?orderBy=${sort.value.column}&order=${sort.value.direction}`)
</script>
<template>
<UTable v-model:sort="sort" :loading="pending" :columns="columns" :rows="data" sort-mode="manual" />
</template>
```
::callout{icon="i-heroicons-light-bulb" to="https://nuxt.com/docs/api/composables/use-fetch#params" target="_blank"}
We pass a function to `useLazyFetch` here make the url reactive but you can use the `query` / `params` options alongside `watch`.
::
#### Custom sorting
Use the `sort-button` prop to customize the sort button in the header. You can pass all the props of the [Button](/elements/button) component to customize it through this prop or globally through `ui.table.default.sortButton`. Its icon defaults to `i-heroicons-arrows-up-down-20-solid`.
::component-card{class="grid"}
@@ -151,43 +225,6 @@ Use the `sort-desc-icon` prop to set a different icon or change it globally in `
You can also customize the entire header cell, read more in the [Slots](#slots) section.
::
#### Reactive sorting
Sometimes you will want to fetch new data depending on the sorted column and direction. You can use the `v-model:sort` to automatically update the `ref` reactive element every time the sorting changes on the Table. You may also use `@update:sort` to call your own function with the sorting data.
For example, we can take advantage of `useLazyRefresh` computed URL to automatically fetch the data depending on the sorting column and direction every time the `sort` reactive element changes.
```vue
<script setup>
// Ensure it uses `ref` instead of `reactive`.
const sort = ref({
column: 'name',
direction: 'desc'
})
const columns = [...]
const { data, pending } = useLazyFetch(() => {
return `/api/users?orderBy=${sort.value.column}&order=${sort.value.direction}`
})
</script>
<template>
<UTable v-model:sort="sort" :loading="pending" :columns="columns" :rows="data" />
</template>
```
The initial value of `sort` will be respected as the initial sort column and direction, as well as each column default sorting direction.
::component-example{class="grid"}
---
padding: false
component: 'table-example-reactive-sorting'
componentProps:
class: 'flex-1'
---
::
### Selectable
Use a `v-model` to make the table selectable. The `v-model` will be an array of the selected rows.

View File

@@ -27,6 +27,12 @@ You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/com
Learn how to build a Tailwind like vertical navigation in the [Examples](/getting-started/examples#verticalnavigation) page.
::
## Sections
Group your navigation links into distinct sections, separated by a divider. You can do this by passing an array of arrays to the `links` prop of the VerticalNavigation component.
:component-example{component="vertical-navigation-example-sections"}
## Slots
You can use slots to customize links display.

View File

@@ -46,6 +46,22 @@ props:
---
::
### Disabled :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Use the `disabled` prop to disable all the buttons.
::component-card
---
baseProps:
modelValue: 1
total: 100
showLast: true
showFirst: true
props:
disabled: true
---
::
### Active / Inactive
Use the `active-button` and `inactive-button` props to customize the active and inactive buttons of the Pagination.

View File

@@ -21,7 +21,7 @@ Use the `mode` prop to switch between `click` and `hover` modes.
### Manual
Use the `open` prop to manually control showing the panel.
Use a `v-model:open` to manually control the state. In this example, press :shortcut{value="O"} to toggle the popover.
:component-example{component="popover-example-open"}
@@ -29,10 +29,6 @@ Use the `open` prop to manually control showing the panel.
:component-example{component="popover-example-overlay"}
::callout{icon="i-heroicons-light-bulb"}
Clicking on the `overlay` emits `update:open`. If you are manually controlling the `open` prop, you will need to use a [`v-model` argument](https://vuejs.org/guide/components/v-model.html#v-model-arguments) (`v-model:open`).
::
## Popper
Use the `popper` prop to customize the popper instance.

View File

@@ -133,7 +133,7 @@ excludedProps:
### Timeout
Use the `timeout` prop to configure how long the Notification will remain. Set it to `0` to disable the timeout.
Use the `timeout` prop to configure how long the Notification will remain. The default value is `5000`, set it to `0` to disable the timeout.
You will see a progress bar at the bottom of the Notification which will indicate the remaining time. When hovering the Notification, the progress bar will be paused.

View File

@@ -17,8 +17,8 @@ export default defineNuxtConfig({
].filter(Boolean),
modules: [
'@nuxt/content',
'@nuxt/image',
'nuxt-og-image',
// '@nuxt/devtools',
// '@nuxthq/studio',
module,
'@nuxtjs/fontaine',
@@ -86,7 +86,8 @@ export default defineNuxtConfig({
'/api/search.json',
'/api/releases.json',
'/api/pulls.json'
]
],
ignore: !process.env.NUXT_UI_PRO_PATH && !process.env.NUXT_GITHUB_TOKEN ? ['/pro'] : []
}
},
componentMeta: {

View File

@@ -5,32 +5,32 @@
"@nuxt/ui": "workspace:latest"
},
"devDependencies": {
"@iconify-json/heroicons": "^1.1.15",
"@iconify-json/simple-icons": "^1.1.82",
"@nuxt/content": "^2.9.0",
"@nuxt/devtools": "^1.0.4",
"@iconify-json/heroicons": "^1.1.19",
"@iconify-json/simple-icons": "^1.1.87",
"@nuxt/content": "^2.10.0",
"@nuxt/devtools": "^1.0.6",
"@nuxt/eslint-config": "^0.2.0",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.6.0-28371586.2e2b852",
"@nuxthq/studio": "^1.0.5",
"@nuxt/image": "^1.1.0",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.6.1-28413612.408e456",
"@nuxthq/studio": "^1.0.6",
"@nuxtjs/fontaine": "^0.4.1",
"@nuxtjs/google-fonts": "^3.1.0",
"@nuxtjs/mdc": "^0.2.8",
"@nuxtjs/google-fonts": "^3.1.3",
"@nuxtjs/plausible": "^0.2.4",
"@octokit/rest": "^20.0.2",
"@vueuse/nuxt": "^10.7.0",
"date-fns": "^2.30.0",
"eslint": "^8.55.0",
"@vueuse/nuxt": "^10.7.1",
"date-fns": "^3.1.0",
"eslint": "^8.56.0",
"joi": "^17.11.0",
"nuxt": "^3.8.2",
"nuxt": "^3.9.1",
"nuxt-cloudflare-analytics": "^1.0.8",
"nuxt-component-meta": "^0.6.0",
"nuxt-og-image": "^2.2.4",
"prettier": "^3.1.0",
"prettier": "^3.1.1",
"typescript": "^5.3.3",
"ufo": "^1.3.2",
"v-calendar": "^3.1.2",
"valibot": "^0.21.0",
"yup": "^1.3.2",
"valibot": "^0.25.0",
"yup": "^1.3.3",
"zod": "^3.22.4"
}
}

View File

@@ -41,6 +41,8 @@ const { data: pulls } = await useLazyFetch('/api/pulls.json', { default: () => [
const dates = computed(() => {
const first = releases.value[releases.value.length - 1]
if (!first) return []
const days = eachDayOfInterval({ start: new Date(first.published_at), end: new Date() })
return days.reverse().map(day => {

View File

@@ -1,6 +1,6 @@
{
"name": "@nuxt/ui",
"version": "2.11.1",
"version": "2.12.0",
"repository": "nuxt/ui",
"homepage": "https://ui.nuxt.com",
"license": "MIT",
@@ -32,51 +32,54 @@
"test": "vitest"
},
"dependencies": {
"@egoist/tailwindcss-icons": "^1.4.0",
"@egoist/tailwindcss-icons": "^1.7.2",
"@headlessui/tailwindcss": "^0.2.0",
"@headlessui/vue": "^1.7.16",
"@iconify-json/heroicons": "^1.1.15",
"@nuxt/kit": "^3.8.2",
"@iconify-json/heroicons": "^1.1.19",
"@nuxt/kit": "^3.9.1",
"@nuxtjs/color-mode": "^3.3.2",
"@nuxtjs/tailwindcss": "^6.10.1",
"@nuxtjs/tailwindcss": "^6.10.3",
"@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@vueuse/core": "^10.7.0",
"@vueuse/integrations": "^10.7.0",
"@vueuse/math": "^10.7.0",
"defu": "^6.1.3",
"@vueuse/core": "^10.7.1",
"@vueuse/integrations": "^10.7.1",
"@vueuse/math": "^10.7.1",
"defu": "^6.1.4",
"fuse.js": "^6.6.2",
"nuxt-icon": "^0.6.7",
"nuxt-icon": "^0.6.8",
"ohash": "^1.1.3",
"pathe": "^1.1.1",
"scule": "^1.1.1",
"tailwind-merge": "^2.1.0",
"tailwindcss": "^3.3.6"
"tailwind-merge": "^2.2.0",
"tailwindcss": "^3.4.1"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.2.0",
"@nuxt/module-builder": "^0.5.4",
"@nuxt/module-builder": "^0.5.5",
"@nuxt/test-utils": "^3.9.0",
"@release-it/conventional-changelog": "^8.0.1",
"eslint": "^8.55.0",
"@vue/test-utils": "^2.4.3",
"eslint": "^8.56.0",
"happy-dom": "^12.10.3",
"joi": "^17.11.0",
"nuxt": "^3.8.2",
"nuxt-vitest": "^0.11.5",
"release-it": "^17.0.0",
"nuxt": "^3.9.1",
"release-it": "^17.0.1",
"typescript": "^5.3.3",
"unbuild": "^2.0.0",
"valibot": "^0.21.0",
"vitest": "^0.33.0",
"vue-tsc": "^1.8.25",
"yup": "^1.3.2",
"valibot": "^0.25.0",
"vitest": "^1.1.3",
"vitest-environment-nuxt": "^1.0.0",
"vue-tsc": "^1.8.27",
"yup": "^1.3.3",
"zod": "^3.22.4"
},
"resolutions": {
"@nuxt/kit": "3.8.2",
"@nuxt/schema": "3.8.2",
"vue": "3.3.8"
"@nuxt/kit": "3.9.1",
"@nuxt/schema": "3.9.1",
"vue": "3.3.13",
"tailwindcss": "3.4.1"
}
}

5048
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -143,7 +143,7 @@ export default defineNuxtModule<ModuleOptions>({
tailwindConfig.safelist.push(...generateSafelist(options.safelistColors || [], colors))
tailwindConfig.plugins = tailwindConfig.plugins || []
tailwindConfig.plugins.push(iconsPlugin(Array.isArray(options.icons) || options.icons === 'all' ? { collections: getIconCollections(options.icons) } : typeof options.icons === 'object' ? options.icons as IconsPluginOptions : {}))
tailwindConfig.plugins.push(iconsPlugin(Array.isArray(options.icons) ? { collections: getIconCollections(options.icons) } : typeof options.icons === 'object' ? options.icons as IconsPluginOptions : {}))
})
createTemplates(nuxt)

View File

@@ -67,10 +67,11 @@
</template>
<script lang="ts">
import { ref, computed, defineComponent, toRaw, toRef } from 'vue'
import { computed, defineComponent, toRaw, toRef } from 'vue'
import type { PropType } from 'vue'
import { upperFirst } from 'scule'
import { defu } from 'defu'
import { useVModel } from '@vueuse/core'
import UButton from '../elements/Button.vue'
import UIcon from '../elements/Icon.vue'
import UCheckbox from '../forms/Checkbox.vue'
@@ -87,6 +88,18 @@ function defaultComparator<T> (a: T, z: T): boolean {
return a === z
}
function defaultSort (a: any, b: any, direction: 'asc' | 'desc') {
if (a === b) {
return 0
}
if (direction === 'asc') {
return a < b ? -1 : 1
} else {
return a > b ? -1 : 1
}
}
export default defineComponent({
components: {
UButton,
@@ -108,7 +121,7 @@ export default defineComponent({
default: () => []
},
columns: {
type: Array as PropType<{ key: string, sortable?: boolean, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>,
type: Array as PropType<{ key: string, sortable?: boolean, sort?: (a: any, b: any, direction: 'asc' | 'desc') => number, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>,
default: null
},
columnAttribute: {
@@ -119,6 +132,10 @@ export default defineComponent({
type: Object as PropType<{ column: string, direction: 'asc' | 'desc' }>,
default: () => ({})
},
sortMode: {
type: String as PropType<'manual' | 'auto'>,
default: 'auto'
},
sortButton: {
type: Object as PropType<Button>,
default: () => config.default.sortButton as Button
@@ -156,14 +173,14 @@ export default defineComponent({
setup (props, { emit, attrs: $attrs }) {
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class'))
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: upperFirst(key), sortable: false })))
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: upperFirst(key), sortable: false, class: undefined, sort: defaultSort })))
const sort = ref(defu({}, props.sort, { column: null, direction: 'asc' }))
const sort = useVModel(props, 'sort', emit, { passive: true, defaultValue: defu({}, props.sort, { column: null, direction: 'asc' }) })
const defaultSort = { column: sort.value.column, direction: null }
const savedSort = { column: sort.value.column, direction: null }
const rows = computed(() => {
if (!sort.value?.column) {
if (!sort.value?.column || props.sortMode === 'manual') {
return props.rows
}
@@ -173,15 +190,9 @@ export default defineComponent({
const aValue = get(a, column)
const bValue = get(b, column)
if (aValue === bValue) {
return 0
}
const sort = columns.value.find((col) => col.key === column)?.sort ?? defaultSort
if (direction === 'asc') {
return aValue < bValue ? -1 : 1
} else {
return aValue > bValue ? -1 : 1
}
return sort(aValue, bValue, direction)
})
})
@@ -227,15 +238,13 @@ export default defineComponent({
const direction = !column.direction || column.direction === 'asc' ? 'desc' : 'asc'
if (sort.value.direction === direction) {
sort.value = defu({}, defaultSort, { column: null, direction: 'asc' })
sort.value = defu({}, savedSort, { column: null, direction: 'asc' })
} else {
sort.value.direction = sort.value.direction === 'asc' ? 'desc' : 'asc'
sort.value = { column: sort.value.column, direction: sort.value.direction === 'asc' ? 'desc' : 'asc' }
}
} else {
sort.value = { column: column.key, direction: column.direction || 'asc' }
}
emit('update:sort', sort.value)
}
function onSelect (row) {
@@ -267,7 +276,7 @@ export default defineComponent({
}
}
function getRowData (row: Object, rowKey: string | string[], defaultValue: any = 'Failed to get cell value') {
function getRowData (row: Object, rowKey: string | string[], defaultValue: any = '') {
return get(row, rowKey, defaultValue)
}

View File

@@ -119,7 +119,8 @@ export default defineComponent({
})
}
function onEnter (el: HTMLElement, done) {
function onEnter (_el: Element, done: () => void) {
const el = _el as HTMLElement
el.style.height = '0'
el.offsetHeight // Trigger a reflow, flushing the CSS changes
el.style.height = el.scrollHeight + 'px'
@@ -127,16 +128,19 @@ export default defineComponent({
el.addEventListener('transitionend', done, { once: true })
}
function onBeforeLeave (el: HTMLElement) {
function onBeforeLeave (_el: Element) {
const el = _el as HTMLElement
el.style.height = el.scrollHeight + 'px'
el.offsetHeight // Trigger a reflow, flushing the CSS changes
}
function onAfterEnter (el: HTMLElement) {
function onAfterEnter (_el: Element) {
const el = _el as HTMLElement
el.style.height = 'auto'
}
function onLeave (el: HTMLElement, done) {
function onLeave (_el: Element, done: () => void) {
const el = _el as HTMLElement
el.style.height = '0'
el.addEventListener('transitionend', done, { once: true })

View File

@@ -17,12 +17,12 @@
</p>
<div v-if="(description || $slots.description) && actions.length" :class="ui.actions">
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="action.click" />
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
</div>
</div>
<div v-if="closeButton || (!description && !$slots.description && actions.length)" :class="twMerge(ui.actions, 'mt-0')">
<template v-if="!description && !$slots.description && actions.length">
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="action.click" />
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
</template>
<UButton v-if="closeButton" aria-label="Close" v-bind="{ ...(ui.default.closeButton || {}), ...closeButton }" @click.stop="$emit('close')" />
@@ -39,7 +39,7 @@ import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue'
import { useUI } from '../../composables/useUI'
import type { Avatar, Button, AlertColor, AlertVariant, Strategy } from '../../types'
import type { Avatar, Button, AlertColor, AlertVariant, AlertAction, Strategy } from '../../types'
import { mergeConfig } from '../../utils'
// @ts-expect-error
import appConfig from '#build/app.config'
@@ -76,7 +76,7 @@ export default defineComponent({
default: () => config.default.closeButton as unknown as Button
},
actions: {
type: Array as PropType<(Button & { click?: Function })[]>,
type: Array as PropType<AlertAction[]>,
default: () => []
},
color: {
@@ -121,11 +121,18 @@ export default defineComponent({
), props.class)
})
function onAction (action: AlertAction) {
if (action.click) {
action.click()
}
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
alertClass,
onAction,
twMerge
}
}

View File

@@ -1,4 +1,5 @@
<template>
<!-- eslint-disable-next-line vue/no-template-shadow -->
<HMenu v-slot="{ open }" as="div" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave">
<HMenuButton
ref="trigger"
@@ -19,16 +20,17 @@
<Transition appear v-bind="ui.transition">
<div>
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(ui.arrow)" />
<HMenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.height]" static>
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
<NuxtLink v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ href, target, rel, navigate, isExternal }" v-bind="omit(item, ['label', 'labelClass', 'slot', 'icon', 'iconClass', 'avatar', 'shortcuts', 'disabled', 'click'])" custom>
<NuxtLink v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ href, target, rel, navigate, isExternal }" v-bind="omit(item, ['label', 'labelClass', 'slot', 'icon', 'iconClass', 'avatar', 'shortcuts', 'disabled', 'class', 'click'])" custom>
<HMenuItem v-slot="{ active, disabled: itemDisabled, close }" :disabled="item.disabled">
<component
:is="!!href ? 'a' : 'button'"
:href="!itemDisabled ? href : undefined"
:rel="rel"
:target="target"
:class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]"
:class="twMerge(twJoin(ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled), item.class)"
@click="onClick($event, item, { href, navigate, close, isExternal })"
>
<slot :name="item.slot || 'item'" :item="item">
@@ -53,7 +55,7 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed, toRef, onMounted, resolveComponent } from 'vue'
import { defineComponent, ref, computed, watch, toRef, onMounted, resolveComponent } from 'vue'
import type { PropType } from 'vue'
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
import { defu } from 'defu'
@@ -92,6 +94,10 @@ export default defineComponent({
default: 'click',
validator: (value: string) => ['click', 'hover'].includes(value)
},
open: {
type: Boolean,
default: undefined
},
disabled: {
type: Boolean,
default: false
@@ -117,7 +123,8 @@ export default defineComponent({
default: () => ({})
}
},
setup (props) {
emits: ['update:open'],
setup (props, { emit }) {
const { ui, attrs } = useUI('dropdown', toRef(props, 'ui'), config, toRef(props, 'class'))
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
@@ -131,21 +138,46 @@ export default defineComponent({
let closeTimeout: NodeJS.Timeout | null = null
onMounted(() => {
setTimeout(() => {
// @ts-expect-error internals
const menuProvides = trigger.value?.$.provides
if (!menuProvides) {
return
}
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
}, 200)
// @ts-expect-error internals
const menuProvides = trigger.value?.$.provides
if (!menuProvides) {
return
}
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
if (props.open) {
menuApi.value?.openMenu()
}
})
const containerStyle = computed(() => {
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
if (props.mode !== 'hover') {
return {}
}
return props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
const placement = popper.value.placement?.split('-')[0]
const padding = `${offsetDistance}px`
if (placement === 'top' || placement === 'bottom') {
return {
paddingTop: padding,
paddingBottom: padding
}
} else if (placement === 'left' || placement === 'right') {
return {
paddingLeft: padding,
paddingRight: padding
}
} else {
return {
paddingTop: padding,
paddingBottom: padding,
paddingLeft: padding,
paddingRight: padding
}
}
})
function onMouseOver () {
@@ -200,6 +232,23 @@ export default defineComponent({
}
}
watch(() => props.open, (newValue: boolean, oldValue: boolean) => {
if (!menuApi.value) return
if (oldValue === undefined || newValue === oldValue) return
if (newValue) {
menuApi.value.openMenu()
} else {
menuApi.value.closeMenu()
}
})
watch(() => menuApi.value?.menuState, (newValue: number, oldValue: number) => {
if (oldValue === undefined || newValue === oldValue) return
emit('update:open', newValue === 0)
})
const NuxtLink = resolveComponent('NuxtLink')
return {

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { provide, ref, type PropType, defineComponent } from 'vue'
import { provide, ref, type PropType, defineComponent, onUnmounted, onMounted } from 'vue'
import { useEventBus } from '@vueuse/core'
import type { ZodSchema } from 'zod'
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
@@ -51,10 +51,16 @@ export default defineComponent({
setup (props, { expose, emit }) {
const bus = useEventBus<FormEvent>(`form-${uid()}`)
bus.on(async (event) => {
if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {
await validate(event.path, { silent: true })
}
onMounted(() => {
bus.on(async (event) => {
if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {
await validate(event.path, { silent: true })
}
})
})
onUnmounted(() => {
bus.reset()
})
const errors = ref<FormError[]>([])
@@ -104,7 +110,8 @@ export default defineComponent({
return props.state
}
async function onSubmit (event: SubmitEvent) {
async function onSubmit (payload: Event) {
const event = payload as SubmitEvent
try {
if (props.validateOn?.includes('submit')) {
await validate()
@@ -143,6 +150,9 @@ export default defineComponent({
errors.value = errs
}
},
async submit () {
await onSubmit(new Event('submit'))
},
getErrors (path?: string) {
if (path) {
return errors.value.filter((err) => err.path === path)
@@ -151,7 +161,7 @@ export default defineComponent({
},
clear (path?: string) {
if (path) {
errors.value = errors.value.filter((err) => err.path === path)
errors.value = errors.value.filter((err) => err.path !== path)
} else {
errors.value = []
}

View File

@@ -8,7 +8,7 @@
:type="type"
:required="required"
:placeholder="placeholder"
:disabled="disabled || loading"
:disabled="disabled"
:class="inputClass"
v-bind="attrs"
@input="onInput"

View File

@@ -4,7 +4,7 @@
:by="by"
:name="name"
:model-value="modelValue"
:disabled="disabled || loading"
:disabled="disabled"
as="div"
:class="ui.wrapper"
@update:model-value="onUpdate"
@@ -12,16 +12,15 @@
<div :class="uiMenu.trigger">
<HComboboxInput
:id="inputId"
ref="input"
:name="name"
:required="required"
:placeholder="placeholder"
:disabled="disabled || loading"
:disabled="disabled"
:class="inputClass"
autocomplete="off"
v-bind="attrs"
:display-value="() => ['string', 'number'].includes(typeof modelValue) ? modelValue : modelValue[optionAttribute]"
@change="query = $event.target.value"
:display-value="() => query ? query : ['string', 'number'].includes(typeof modelValue) ? modelValue : modelValue[optionAttribute]"
@change="onChange"
/>
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
@@ -100,7 +99,7 @@ import {
ComboboxOption as HComboboxOption,
ComboboxInput as HComboboxInput
} from '@headlessui/vue'
import { computedAsync } from '@vueuse/core'
import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
@@ -135,6 +134,10 @@ export default defineComponent({
type: [String, Number, Object, Array],
default: ''
},
query: {
type: String,
default: null
},
by: {
type: String,
default: undefined
@@ -231,10 +234,18 @@ export default defineComponent({
type: String,
default: null
},
search: {
type: Function as PropType<((query: string) => Promise<any[]> | any[])>,
default: undefined
},
searchAttributes: {
type: Array,
default: null
},
debounce: {
type: Number,
default: 200
},
popper: {
type: Object as PropType<PopperOptions>,
default: () => ({})
@@ -256,7 +267,7 @@ export default defineComponent({
default: () => ({})
}
},
emits: ['update:modelValue', 'open', 'close', 'change'],
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change'],
setup (props, { emit, slots }) {
const { ui, attrs } = useUI('input', toRef(props, 'ui'), config, toRef(props, 'class'))
@@ -271,7 +282,16 @@ export default defineComponent({
const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value)
const query = ref('')
const internalQuery = ref('')
const query = computed({
get () {
return props.query ?? internalQuery.value
},
set (value) {
internalQuery.value = value
emit('update:query', value)
}
})
const inputClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -346,7 +366,13 @@ export default defineComponent({
)
})
const debouncedSearch = props.search && typeof props.search === 'function' ? useDebounceFn(props.search, props.debounce) : undefined
const filteredOptions = computedAsync(async () => {
if (debouncedSearch) {
return await debouncedSearch(query.value)
}
if (query.value === '') {
return props.options
}
@@ -374,11 +400,16 @@ export default defineComponent({
})
function onUpdate (event: any) {
query.value = ''
emit('update:modelValue', event)
emit('change', event)
emitFormChange()
}
function onChange (event: any) {
query.value = event.target.value
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
@@ -403,8 +434,10 @@ export default defineComponent({
trailingIconClass,
trailingWrapperIconClass,
filteredOptions,
// eslint-disable-next-line vue/no-dupe-keys
query,
onUpdate
onUpdate,
onChange
}
}
})

View File

@@ -12,7 +12,7 @@
:label="option.label"
:model-value="modelValue"
:value="option.value"
:disabled="disabled"
:disabled="option.disabled || disabled"
:ui="uiRadio"
@change="onUpdate(option.value)"
>

View File

@@ -5,7 +5,7 @@
:name="name"
:value="modelValue"
:required="required"
:disabled="disabled || loading"
:disabled="disabled"
:class="selectClass"
v-bind="attrs"
@input="onInput"

View File

@@ -6,7 +6,7 @@
:name="name"
:model-value="modelValue"
:multiple="multiple"
:disabled="disabled || loading"
:disabled="disabled"
as="div"
:class="ui.wrapper"
@update:model-value="onUpdate"
@@ -28,7 +28,7 @@
:class="uiMenu.trigger"
>
<slot :open="open" :disabled="disabled" :loading="loading">
<button :id="inputId" :class="selectClass" :disabled="disabled || loading" type="button" v-bind="attrs">
<button :id="inputId" :class="selectClass" :disabled="disabled" type="button" v-bind="attrs">
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
@@ -58,14 +58,13 @@
<component :is="searchable ? 'HComboboxOptions' : 'HListboxOptions'" static :class="[uiMenu.base, uiMenu.ring, uiMenu.rounded, uiMenu.shadow, uiMenu.background, uiMenu.padding, uiMenu.height]">
<HComboboxInput
v-if="searchable"
ref="searchInput"
:display-value="() => query"
name="q"
:placeholder="searchablePlaceholder"
autofocus
autocomplete="off"
:class="uiMenu.input"
@change="query = $event.target.value"
@change="onChange"
/>
<component
:is="searchable ? 'HComboboxOption' : 'HListboxOption'"
@@ -98,11 +97,11 @@
</li>
</component>
<component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template">
<component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && createOption" v-slot="{ active, selected }" :value="createOption" as="template">
<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">
<slot name="option-create" :option="queryOption" :active="active" :selected="selected">
<span :class="uiMenu.option.create">Create "{{ queryOption[optionAttribute] }}"</span>
<slot name="option-create" :option="createOption" :active="active" :selected="selected">
<span :class="uiMenu.option.create">Create "{{ createOption[optionAttribute] }}"</span>
</slot>
</div>
</li>
@@ -126,7 +125,7 @@
<script lang="ts">
import { ref, computed, toRef, watch, defineComponent } from 'vue'
import type { PropType, ComponentPublicInstance } from 'vue'
import type { PropType } from 'vue'
import {
Combobox as HCombobox,
ComboboxButton as HComboboxButton,
@@ -177,6 +176,10 @@ export default defineComponent({
type: [String, Number, Object, Array],
default: ''
},
query: {
type: String,
default: null
},
by: {
type: String,
default: undefined
@@ -247,7 +250,7 @@ export default defineComponent({
},
clearSearchOnClose: {
type: Boolean,
default: () => configMenu.default.clearOnClose
default: () => configMenu.default.clearSearchOnClose
},
debounce: {
type: Number,
@@ -257,6 +260,10 @@ export default defineComponent({
type: Boolean,
default: false
},
showCreateOptionWhen: {
type: String as PropType<'always' | 'empty'>,
default: () => configMenu.default.showCreateOptionWhen
},
placeholder: {
type: String,
default: null
@@ -322,7 +329,7 @@ export default defineComponent({
default: () => ({})
}
},
emits: ['update:modelValue', 'open', 'close', 'change'],
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change'],
setup (props, { emit, slots }) {
const { ui, attrs } = useUI('select', toRef(props, 'ui'), config, toRef(props, 'class'))
@@ -337,8 +344,16 @@ export default defineComponent({
const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value)
const query = ref('')
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
const internalQuery = ref('')
const query = computed({
get () {
return props.query ?? internalQuery.value
},
set (value) {
internalQuery.value = value
emit('update:query', value)
}
})
const selectClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -438,8 +453,21 @@ export default defineComponent({
})
})
const queryOption = computed(() => {
return query.value === '' ? null : { [props.optionAttribute]: query.value }
const createOption = computed(() => {
if (query.value === '') {
return null
}
if (props.showCreateOptionWhen === 'empty' && filteredOptions.value.length) {
return null
}
if (props.showCreateOptionWhen === 'always') {
const existingOption = filteredOptions.value.find(option => ['string', 'number'].includes(typeof option) ? option === query.value : option[props.optionAttribute] === query.value)
if (existingOption) {
return null
}
}
return ['string', 'number'].includes(typeof props.modelValue) ? query.value : { [props.optionAttribute]: query.value }
})
function clearOnClose () {
@@ -459,17 +487,15 @@ export default defineComponent({
})
function onUpdate (event: any) {
if (query.value && searchInput.value?.$el) {
query.value = ''
// explicitly set input text because `ComboboxInput` `displayValue` is not reactive
searchInput.value.$el.value = ''
}
emit('update:modelValue', event)
emit('change', event)
emitFormChange()
}
function onChange (event: any) {
query.value = event.target.value
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
@@ -494,9 +520,11 @@ export default defineComponent({
trailingIconClass,
trailingWrapperIconClass,
filteredOptions,
queryOption,
createOption,
// eslint-disable-next-line vue/no-dupe-keys
query,
onUpdate
onUpdate,
onChange
}
}
})

View File

@@ -89,7 +89,7 @@ export default defineComponent({
default: () => ({})
}
},
emits: ['update:modelValue'],
emits: ['update:modelValue', 'change'],
setup (props, { emit }) {
const { ui, attrs } = useUI('toggle', toRef(props, 'ui'), config)

View File

@@ -4,7 +4,7 @@
<UButton
v-if="firstButton && showFirst"
:size="size"
:disabled="!canGoFirstOrPrev"
:disabled="!canGoFirstOrPrev || disabled"
:class="[ui.base, ui.rounded]"
v-bind="{ ...(ui.default.firstButton || {}), ...firstButton }"
:ui="{ rounded: '' }"
@@ -17,7 +17,7 @@
<UButton
v-if="prevButton"
:size="size"
:disabled="!canGoFirstOrPrev"
:disabled="!canGoFirstOrPrev || disabled"
:class="[ui.base, ui.rounded]"
v-bind="{ ...(ui.default.prevButton || {}), ...prevButton }"
:ui="{ rounded: '' }"
@@ -30,6 +30,7 @@
v-for="(page, index) of displayedPages"
:key="`${page}-${index}`"
:size="size"
:disabled="disabled"
:label="`${page}`"
v-bind="page === currentPage ? { ...(ui.default.activeButton || {}), ...activeButton } : { ...(ui.default.inactiveButton || {}), ...inactiveButton }"
:class="[{ 'pointer-events-none': typeof page === 'string', 'z-[1]': page === currentPage }, ui.base, ui.rounded]"
@@ -41,7 +42,7 @@
<UButton
v-if="nextButton"
:size="size"
:disabled="!canGoLastOrNext"
:disabled="!canGoLastOrNext || disabled"
:class="[ui.base, ui.rounded]"
v-bind="{ ...(ui.default.nextButton || {}), ...nextButton }"
:ui="{ rounded: '' }"
@@ -54,7 +55,7 @@
<UButton
v-if="lastButton && showLast"
:size="size"
:disabled="!canGoLastOrNext"
:disabled="!canGoLastOrNext || disabled"
:class="[ui.base, ui.rounded]"
v-bind="{ ...(ui.default.lastButton || {}), ...lastButton }"
:ui="{ rounded: '' }"
@@ -105,6 +106,10 @@ export default defineComponent({
return value >= 5 && value < Number.MAX_VALUE
}
},
disabled: {
type: Boolean,
default: false
},
size: {
type: String as PropType<ButtonSize>,
default: () => config.default.size,

View File

@@ -1,49 +1,58 @@
<template>
<nav :class="ui.wrapper" v-bind="attrs">
<ULink
v-for="(link, index) of links"
v-slot="{ isActive }"
:key="index"
v-bind="omit(link, ['label', 'labelClass', 'icon', 'iconClass', 'avatar', 'badge', 'click'])"
:class="[ui.base, ui.padding, ui.width, ui.ring, ui.rounded, ui.font, ui.size]"
:active-class="ui.active"
:inactive-class="ui.inactive"
@click="link.click"
@keyup.enter="$event.target.blur()"
>
<slot name="avatar" :link="link" :is-active="isActive">
<UAvatar
v-if="link.avatar"
v-bind="{ size: ui.avatar.size, ...link.avatar }"
:class="[ui.avatar.base]"
/>
</slot>
<slot name="icon" :link="link" :is-active="isActive">
<UIcon
v-if="link.icon"
:name="link.icon"
:class="twMerge(twJoin(ui.icon.base, isActive ? ui.icon.active : ui.icon.inactive), link.iconClass)"
/>
</slot>
<slot :link="link" :is-active="isActive">
<span v-if="link.label" :class="twMerge(ui.label, link.labelClass)">{{ link.label }}</span>
</slot>
<slot name="badge" :link="link" :is-active="isActive">
<span v-if="link.badge" :class="[ui.badge.base, isActive ? ui.badge.active : ui.badge.inactive]">
{{ link.badge }}
</span>
</slot>
</ULink>
<ul v-for="(section, sectionIndex) of linkSections" :key="`linkSection${sectionIndex}`">
<li v-for="(link, index) of section" :key="`linkSection${sectionIndex}-${index}`">
<ULink
v-slot="{ isActive }"
v-bind="omit(link, ['label', 'labelClass', 'icon', 'iconClass', 'avatar', 'badge', 'click'])"
:class="[ui.base, ui.padding, ui.width, ui.ring, ui.rounded, ui.font, ui.size]"
:active-class="ui.active"
:inactive-class="ui.inactive"
@click="link.click"
@keyup.enter="$event.target.blur()"
>
<slot name="avatar" :link="link" :is-active="isActive">
<UAvatar
v-if="link.avatar"
v-bind="{ size: ui.avatar.size, ...link.avatar }"
:class="[ui.avatar.base]"
/>
</slot>
<slot name="icon" :link="link" :is-active="isActive">
<UIcon
v-if="link.icon"
:name="link.icon"
:class="twMerge(twJoin(ui.icon.base, isActive ? ui.icon.active : ui.icon.inactive), link.iconClass)"
/>
</slot>
<slot :link="link" :is-active="isActive">
<span v-if="link.label" :class="twMerge(ui.label, link.labelClass)">
<span v-if="isActive" class="sr-only">
Current page:
</span>
{{ link.label }}
</span>
</slot>
<slot name="badge" :link="link" :is-active="isActive">
<span v-if="link.badge" :class="[ui.badge.base, isActive ? ui.badge.active : ui.badge.inactive]">
{{ link.badge }}
</span>
</slot>
</ULink>
</li>
<UDivider v-if="sectionIndex < linkSections.length - 1" :ui="ui.divider" />
</ul>
</nav>
</template>
<script lang="ts">
import { toRef, defineComponent } from 'vue'
import { toRef, defineComponent, computed } from 'vue'
import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import ULink from '../elements/Link.vue'
import UDivider from '../layout/Divider.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig, omit } from '../../utils'
import type { VerticalNavigationLink, Strategy } from '../../types'
@@ -57,12 +66,13 @@ export default defineComponent({
components: {
UIcon,
UAvatar,
ULink
ULink,
UDivider
},
inheritAttrs: false,
props: {
links: {
type: Array as PropType<VerticalNavigationLink[]>,
type: Array as PropType<VerticalNavigationLink[][] | VerticalNavigationLink[]>,
default: () => []
},
class: {
@@ -77,11 +87,16 @@ export default defineComponent({
setup (props) {
const { ui, attrs } = useUI('verticalNavigation', toRef(props, 'ui'), config, toRef(props, 'class'))
const linkSections = computed(() => {
return (Array.isArray(props.links[0]) ? props.links : [props.links]) as VerticalNavigationLink[][]
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
omit,
linkSections,
twMerge,
twJoin
}

View File

@@ -1,5 +1,6 @@
<template>
<HPopover ref="popover" v-slot="{ open: headlessOpen, close }" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave">
<!-- eslint-disable-next-line vue/no-template-shadow -->
<HPopover ref="popover" v-slot="{ open, close }" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave">
<HPopoverButton
ref="trigger"
as="div"
@@ -8,7 +9,7 @@
role="button"
@mouseover="onMouseOver"
>
<slot :open="(open !== undefined) ? open : headlessOpen" :close="close">
<slot :open="open" :close="close">
<button :disabled="disabled">
Open
</button>
@@ -16,16 +17,16 @@
</HPopoverButton>
<Transition v-if="overlay" appear v-bind="ui.overlay.transition">
<div v-if="(open !== undefined) ? open : headlessOpen" :class="[ui.overlay.base, ui.overlay.background]" @click="$emit('update:open')" />
<div v-if="open" :class="[ui.overlay.base, ui.overlay.background]" />
</Transition>
<div v-if="(open !== undefined) ? open : headlessOpen" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver">
<div v-if="open" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver">
<Transition appear v-bind="ui.transition">
<div>
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(ui.arrow)" />
<HPopoverPanel :class="[ui.base, ui.background, ui.ring, ui.rounded, ui.shadow]" static>
<slot name="panel" :open="(open !== undefined) ? open : headlessOpen" :close="close" />
<slot name="panel" :open="open" :close="close" />
</HPopoverPanel>
</div>
</Transition>
@@ -94,7 +95,7 @@ export default defineComponent({
default: () => ({})
}
},
emits: ['update:open', 'open', 'close'],
emits: ['update:open'],
setup (props, { emit }) {
const { ui, attrs } = useUI('popover', toRef(props, 'ui'), config, toRef(props, 'class'))
@@ -102,8 +103,8 @@ export default defineComponent({
const [trigger, container] = usePopper(popper.value)
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/popover/popover.ts#L151
const popover = ref<any>(null)
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/popover/popover.ts#L151
const popoverApi = ref<any>(null)
let openTimeout: NodeJS.Timeout | null = null
@@ -116,18 +117,39 @@ export default defineComponent({
}
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
if (props.open) {
popoverApi.value?.togglePopover()
}
})
const containerStyle = computed(() => {
if (props.mode !== 'hover') {
return {}
}
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
const placement = popper.value.placement?.split('-')[0]
const padding = `${offsetDistance}px`
return props.mode === 'hover' ? {
paddingTop: padding,
paddingBottom: padding,
paddingLeft: padding,
paddingRight: padding
} : {}
if (placement === 'top' || placement === 'bottom') {
return {
paddingTop: padding,
paddingBottom: padding
}
} else if (placement === 'left' || placement === 'right') {
return {
paddingLeft: padding,
paddingRight: padding
}
} else {
return {
paddingTop: padding,
paddingBottom: padding,
paddingLeft: padding,
paddingRight: padding
}
}
})
function onMouseOver () {
@@ -170,9 +192,22 @@ export default defineComponent({
}, props.closeDelay)
}
watch(() => props.open, (newValue: boolean, oldValue: boolean) => {
if (!popoverApi.value) return
if (oldValue === undefined || newValue === oldValue) return
if (newValue) {
// No `openPopover` method and `popoverApi.value.togglePopover` won't work because of the `watch` below
popoverApi.value.popoverState = 0
} else {
popoverApi.value.closePopover()
}
})
watch(() => popoverApi.value?.popoverState, (newValue: number, oldValue: number) => {
if (oldValue === undefined) return
emit(newValue === 0 ? 'open' : 'close')
if (oldValue === undefined || newValue === oldValue) return
emit('update:open', newValue === 0)
})
return {

View File

@@ -9,7 +9,10 @@ export const _useShortcuts = () => {
const activeElement = useActiveElement()
const usingInput = computed(() => {
const usingInput = !!(activeElement.value?.tagName === 'INPUT' || activeElement.value?.tagName === 'TEXTAREA' || activeElement.value?.contentEditable === 'true')
const tagName = activeElement.value?.tagName
const contentEditable = activeElement.value?.contentEditable
const usingInput = !!(tagName === 'INPUT' || tagName === 'TEXTAREA' || contentEditable === 'true' || contentEditable === 'plaintext-only')
if (usingInput) {
return ((activeElement.value as any)?.name as string) || true

View File

@@ -1,7 +1,12 @@
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'
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 interface AlertAction extends Button {
click?: Function
}

View File

@@ -10,5 +10,6 @@ export interface DropdownItem extends NuxtLinkProps {
avatar?: Avatar
shortcuts?: string[]
disabled?: boolean
class?: string
click?: Function
}

View File

@@ -15,6 +15,7 @@ export interface Form<T> {
errors: Ref<FormError[]>
setErrors(errs: FormError[], path?: string): void
getErrors(path?: string): FormError[]
submit(): Promise<void>
}
export type FormSubmitEvent<T> = SubmitEvent & { data: T }

View File

@@ -4,7 +4,7 @@ import inputMenu from './inputMenu'
export default {
...inputMenu,
select: 'inline-flex items-center text-left cursor-default',
input: 'block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 focus:border-inherit sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none',
input: 'block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none',
required: 'absolute inset-0 w-px opacity-0 cursor-default',
label: 'block truncate',
option: {
@@ -22,7 +22,8 @@ export default {
},
default: {
selectedIcon: 'i-heroicons-check-20-solid',
clearOnClose: false
clearSearchOnClose: false,
showCreateOptionWhen: 'empty'
},
arrow: {
...arrow,

View File

@@ -1,5 +1,5 @@
export default {
base: 'overflow-hidden',
base: '',
background: 'bg-white dark:bg-gray-900',
divide: 'divide-y divide-gray-200 dark:divide-gray-800',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
@@ -20,4 +20,4 @@ export default {
background: '',
padding: 'px-4 py-4 sm:px-6'
}
}
}

View File

@@ -23,5 +23,10 @@ export default {
base: 'relative ms-auto inline-block py-0.5 px-2 text-xs rounded-md -me-1 -my-0.5',
active: 'bg-white dark:bg-gray-900',
inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white group-hover:bg-white dark:group-hover:bg-gray-900'
},
divider: {
wrapper: {
base: 'p-2'
}
}
}

View File

@@ -11,7 +11,13 @@ const customTwMerge = extendTailwindMerge<string, string>({
})
const defuTwMerge = createDefu((obj, key, value, namespace) => {
if (namespace !== 'default' && !namespace.startsWith('default.') && typeof obj[key] === 'string' && typeof value === 'string' && obj[key] && value) {
if (namespace === 'default' || namespace.startsWith('default.')) {
return false
}
if (namespace.endsWith('avatar') && key === 'size') {
return false
}
if (typeof obj[key] === 'string' && typeof value === 'string' && obj[key] && value) {
// @ts-ignore
obj[key] = customTwMerge(obj[key], value)
return true

View File

@@ -1,5 +1,6 @@
let _id = 0
export function uid () {
return `nuid-${_id++}`
_id = (_id + 1) % Number.MAX_SAFE_INTEGER
return `nuid-${_id}`
}

View File

@@ -1,46 +1,58 @@
import { describe, expect, it } from 'vitest'
import module from '../src/module'
import { defu } from 'defu'
import { join } from 'pathe'
import { loadNuxt } from '@nuxt/kit'
import { join } from 'path'
import type { NuxtConfig } from '@nuxt/schema'
import type * as tailwindcss from 'tailwindcss'
type TWConfig = tailwindcss.Config;
import type resolveConfig from 'tailwindcss/resolveConfig'
// TODO: fix these anys
async function getTailwindCSSConfig (overrides: any = {}): Promise<[any, any]> {
overrides.modules = [module]
overrides.ssr = overrides.ssr ?? false
overrides.hooks = overrides.hooks ?? {}
return new Promise((resolve) => {
overrides.hooks['tailwindcss:resolvedConfig'] = async (config: any) => {
resolve([config, nuxt])
}
const nuxt = loadNuxt({
cwd: join(process.cwd(), 'fixtures', 'empty'),
dev: false,
overrides
})
async function getTailwindCSSConfig (overrides: Partial<NuxtConfig> = {}) {
let tailwindConfig: ReturnType<typeof resolveConfig<TWConfig>>
const nuxt = await loadNuxt({
ready: true,
cwd: join(process.cwd(), 'fixtures', 'empty'),
dev: false,
overrides: defu(overrides, {
ssr: false,
modules: ['../../src/module'],
hooks: {
'tailwindcss:resolvedConfig' (config) {
tailwindConfig = config
}
}
} satisfies NuxtConfig) as NuxtConfig
})
const nuxtOptions = structuredClone({
plugins: nuxt.options.plugins.map(p => typeof p !== 'string' && ({ src: p.src, mode: p.mode })),
_requiredModules: nuxt.options._requiredModules,
appConfig: nuxt.options.appConfig
})
await nuxt.close()
return {
nuxtOptions,
tailwindConfig
}
}
describe('nuxt', () => {
it('should add plugins and modules to nuxt', async () => {
const [, lnuxt] = await getTailwindCSSConfig()
await lnuxt.then((nuxt: { options: { plugins: any; _requiredModules: any; appConfig: { ui: any } }; close: () => void }) => {
expect(nuxt.options.plugins).toContainEqual(
expect.objectContaining({
src: expect.stringContaining('plugins/colors'),
mode: 'all'
})
)
expect(nuxt.options._requiredModules).toContain({
'@nuxtjs/color-mode': true,
'@nuxtjs/tailwindcss': true
const { nuxtOptions } = await getTailwindCSSConfig()
expect(nuxtOptions.plugins).toContainEqual(
expect.objectContaining({
src: expect.stringContaining('plugins/colors'),
mode: 'all'
})
// default values in appConfig
expect(nuxt.options.appConfig.ui).toContain({
primary: 'green',
gray: 'cool'
})
// TODO: this should be done inside getTailwindCSSConfig
nuxt.close()
)
expect(nuxtOptions._requiredModules).toMatchObject({
'@nuxtjs/color-mode': true,
'@nuxtjs/tailwindcss': true
})
// default values in appConfig
expect(nuxtOptions.appConfig.ui).toMatchObject({
primary: 'green',
gray: 'cool'
})
})
})
@@ -68,7 +80,7 @@ describe('tailwindcss config', () => {
['bg-(plainBlue|primary)-50', '!', /orange/] // the word "orange" should _not_ be found in any safelist pattern
]
])('%s', async (_description, tailwindcss, safelistColors, safelistPatterns) => {
const [config, _nuxt] = await getTailwindCSSConfig({
const { tailwindConfig } = await getTailwindCSSConfig({
ui: {
safelistColors
},
@@ -105,19 +117,15 @@ describe('tailwindcss config', () => {
continue
}
if (negate) {
expect(config.safelist).not.toContainEqual({
expect(tailwindConfig.safelist).not.toContainEqual({
pattern: expect.toBeRegExp(safelistPattern)
})
} else {
expect(config.safelist).toContainEqual({
expect(tailwindConfig.safelist).toContainEqual({
pattern: expect.toBeRegExp(safelistPattern)
})
}
negate = false
}
await _nuxt.then((n: { close: () => void }) => {
n.close()
})
})
})

View File

@@ -1,8 +1,8 @@
import { mountSuspended } from 'nuxt-vitest/utils'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import path from 'path'
export default async function (nameOrHtml: string, options: any, component: any) {
let html
let html: string
const name = path.parse(component.__file).name
if (options === undefined) {
const app = {

View File

@@ -1,34 +1,34 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Button > renders <UButton icon="i-heroicons-pencil-square" size="sm" color="primary" square variant="solid" /> correctly 1`] = `
"<button type=\\"button\\" class=\\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 p-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center\\"><span class=\\"i-heroicons-pencil-square flex-shrink-0 h-5 w-5\\" aria-hidden=\\"true\\"></span>
"<button type="button" class="focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 p-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center"><span class="i-heroicons-pencil-square flex-shrink-0 h-5 w-5" aria-hidden="true"></span>
<!--v-if-->
<!--v-if-->
</button>"
`;
exports[`Button > renders basic case correctly 1`] = `
"<button type=\\"button\\" class=\\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center\\">
"<button type="button" class="focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center">
<!--v-if-->label
<!--v-if-->
</button>"
`;
exports[`Button > renders black solid correctly 1`] = `
"<button type=\\"button\\" class=\\"focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-gray-900 hover:bg-gray-800 disabled:bg-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:disabled:bg-white focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 inline-flex items-center\\">
"<button type="button" class="focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-gray-900 hover:bg-gray-800 disabled:bg-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:disabled:bg-white focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 inline-flex items-center">
<!--v-if-->label
<!--v-if-->
</button>"
`;
exports[`Button > renders leading icon correctly 1`] = `
"<button type=\\"button\\" class=\\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center\\"><span class=\\"heroicons-check flex-shrink-0 h-5 w-5\\" aria-hidden=\\"true\\"></span>label
"<button type="button" class="focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center"><span class="heroicons-check flex-shrink-0 h-5 w-5" aria-hidden="true"></span>label
<!--v-if-->
</button>"
`;
exports[`Button > renders rounded full correctly 1`] = `
"<button type=\\"button\\" class=\\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-full text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center\\">
"<button type="button" class="focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-full text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center">
<!--v-if-->label
<!--v-if-->
</button>"

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import Skeleton from '../../../src/runtime/components/layout/Skeleton.vue'
import { USkeleton } from '#components'
import type { TypeOf } from 'zod'
import ComponentRender from '../component-render'
@@ -7,9 +7,8 @@ describe('Skeleton', () => {
it.each([
[ 'basic case', { } ],
[ '<USkeleton class="h-12 w-12" :ui="{ rounded: \'rounded-full\' }" />' ]
// @ts-ignore
])('renders %s correctly', async (nameOrHtml: string, options: TypeOf<typeof Skeleton.props>) => {
const html = await ComponentRender(nameOrHtml, options, Skeleton)
])('renders %s correctly', async (nameOrHtml: string, options?: TypeOf<typeof USkeleton.props>) => {
const html = await ComponentRender(nameOrHtml, options, USkeleton)
expect(html).toMatchSnapshot()
})
})

View File

@@ -1,5 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Skeleton > renders <USkeleton class="h-12 w-12" :ui="{ rounded: 'rounded-full' }" /> correctly 1`] = `"<div class=\\"animate-pulse bg-gray-100 dark:bg-gray-800 rounded-full h-12 w-12\\"></div>"`;
exports[`Skeleton > renders <USkeleton class="h-12 w-12" :ui="{ rounded: 'rounded-full' }" /> correctly 1`] = `"<div class="animate-pulse bg-gray-100 dark:bg-gray-800 rounded-full h-12 w-12"></div>"`;
exports[`Skeleton > renders basic case correctly 1`] = `"<div class=\\"animate-pulse bg-gray-100 dark:bg-gray-800 rounded-md\\"></div>"`;
exports[`Skeleton > renders basic case correctly 1`] = `"<div class="animate-pulse bg-gray-100 dark:bg-gray-800 rounded-md"></div>"`;

View File

@@ -1,9 +1,7 @@
/// <reference types="vitest" />
import { fileURLToPath } from 'node:url'
import { defineVitestConfig } from 'nuxt-vitest/config'
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
// @ts-ignore
test: {
testTimeout: 20000,
globals: true,