mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 12:39:35 +01:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4230fd63 | ||
|
|
0be676a9ef | ||
|
|
e48b61b5df | ||
|
|
56e0c9a9a0 | ||
|
|
c6841d06a4 | ||
|
|
8508e84958 | ||
|
|
6154ae94a9 | ||
|
|
6384edf92a | ||
|
|
4f3af6cfdb | ||
|
|
893b2466ff | ||
|
|
9807e58f8f | ||
|
|
4124406032 | ||
|
|
3258167a14 | ||
|
|
520624bd64 | ||
|
|
e4b8fffc32 | ||
|
|
5d781112f1 | ||
|
|
1c8122a00b | ||
|
|
0976833753 | ||
|
|
bc00f9c4b2 | ||
|
|
c6aa4215d7 | ||
|
|
3334e2af3d | ||
|
|
3844714644 | ||
|
|
84e6392981 | ||
|
|
c2ef6237d8 | ||
|
|
f735db04d6 | ||
|
|
e8f573b6bb | ||
|
|
288abf239f | ||
|
|
44d93a1cfd | ||
|
|
217840bb41 | ||
|
|
ea2a24b5fe | ||
|
|
4a25a12390 | ||
|
|
d64cb8a6fd | ||
|
|
00d0fd5919 | ||
|
|
30e7a3ca20 | ||
|
|
7d572c81bb | ||
|
|
97a3975197 | ||
|
|
43b999c88e | ||
|
|
7151b7b97d | ||
|
|
ffd20b3991 | ||
|
|
29e64ca963 | ||
|
|
556ee0d9c4 | ||
|
|
debafef0fa | ||
|
|
2d9038bcb0 | ||
|
|
f7f8f06b91 | ||
|
|
56e1fed373 | ||
|
|
648eec31b9 | ||
|
|
d0ce8ee1c4 | ||
|
|
9d8f358139 | ||
|
|
bc6474a9ad | ||
|
|
31924e32f2 | ||
|
|
c963ba688f | ||
|
|
4dee128524 | ||
|
|
4c84839a01 | ||
|
|
fd30022550 | ||
|
|
1a1c640220 | ||
|
|
5c99ae131d | ||
|
|
b22bd70d54 | ||
|
|
0c8ab9d98e | ||
|
|
0fdc8f70b6 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
1
.npmrc
@@ -1,2 +1,3 @@
|
||||
shamefully-hoist=true
|
||||
auto-install-peers=true
|
||||
ignore-workspace-root-check=true
|
||||
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
22
docs/components/content/examples/DropdownExampleOpen.vue
Normal file
22
docs/components/content/examples/DropdownExampleOpen.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -60,5 +60,5 @@ const people = [{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :columns="columns" :rows="people" :sort="{ column: 'title' }" />
|
||||
<UTable :columns="columns" :rows="people" />
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
::
|
||||
::
|
||||
::
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
51
package.json
51
package.json
@@ -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
5048
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = []
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:type="type"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled || loading"
|
||||
:disabled="disabled"
|
||||
:class="inputClass"
|
||||
v-bind="attrs"
|
||||
@input="onInput"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:name="name"
|
||||
:value="modelValue"
|
||||
:required="required"
|
||||
:disabled="disabled || loading"
|
||||
:disabled="disabled"
|
||||
:class="selectClass"
|
||||
v-bind="attrs"
|
||||
@input="onInput"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
5
src/runtime/types/alert.d.ts
vendored
5
src/runtime/types/alert.d.ts
vendored
@@ -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
|
||||
}
|
||||
|
||||
1
src/runtime/types/dropdown.d.ts
vendored
1
src/runtime/types/dropdown.d.ts
vendored
@@ -10,5 +10,6 @@ export interface DropdownItem extends NuxtLinkProps {
|
||||
avatar?: Avatar
|
||||
shortcuts?: string[]
|
||||
disabled?: boolean
|
||||
class?: string
|
||||
click?: Function
|
||||
}
|
||||
|
||||
1
src/runtime/types/form.d.ts
vendored
1
src/runtime/types/form.d.ts
vendored
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
let _id = 0
|
||||
|
||||
export function uid () {
|
||||
return `nuid-${_id++}`
|
||||
_id = (_id + 1) % Number.MAX_SAFE_INTEGER
|
||||
return `nuid-${_id}`
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>"`;
|
||||
|
||||
@@ -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,
|
||||
Reference in New Issue
Block a user