Compare commits

..

1 Commits

Author SHA1 Message Date
Benjamin Canac
fd95a3f646 fix(module): handle tailwindMerge config from app.config 2023-12-15 14:55:28 +01:00
98 changed files with 2876 additions and 4310 deletions

View File

@@ -4,7 +4,7 @@
### 🔗 Linked issue ### 🔗 Linked issue
<!-- If it resolves an open issue, please link the issue here. For example "Resolves #123" --> <!-- Please ensure there is an open issue and mention its number as #123 -->
### ❓ Type of change ### ❓ Type of change
@@ -21,6 +21,7 @@
<!-- Describe your changes in detail --> <!-- Describe your changes in detail -->
<!-- Why is this change required? What problem does it solve? --> <!-- Why is this change required? What problem does it solve? -->
<!-- If it resolves an open issue, please link to the issue here. For example "Resolves #1337" -->
### 📝 Checklist ### 📝 Checklist

View File

@@ -45,7 +45,7 @@ jobs:
run: | run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4 - uses: actions/cache@v3
name: Setup pnpm cache name: Setup pnpm cache
with: with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}

View File

@@ -38,7 +38,7 @@ jobs:
run: | run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4 - uses: actions/cache@v3
name: Setup pnpm cache name: Setup pnpm cache
with: with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}

1
.npmrc
View File

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

View File

@@ -1,77 +1,5 @@
# Changelog # Changelog
## [2.12.2](https://github.com/nuxt/ui/compare/v2.12.1...v2.12.2) (2024-01-18)
### Bug Fixes
* **link:** improve nuxt link `rel` type ([05e90aa](https://github.com/nuxt/ui/commit/05e90aa1d13ab1772189d33278f482405ff88975))
## [2.12.1](https://github.com/nuxt/ui/compare/v2.12.0...v2.12.1) (2024-01-18)
### Bug Fixes
* **Button:** inherit nuxt link props without breaking `nuxt-component-meta` ([d3e19dc](https://github.com/nuxt/ui/commit/d3e19dc65a530201c3adc7738e95e5a09b0a9274)), closes [#578](https://github.com/nuxt/ui/issues/578)
* **Button:** pass-through nuxt link props to `ULink` ([a44bfc8](https://github.com/nuxt/ui/commit/a44bfc85114bed15ed25bb8c79d7ed52adc8d43c))
* **InputMenu:** take `option-attribute` into account to display label ([1a93791](https://github.com/nuxt/ui/commit/1a937919a26546cfd7edb3f6a11ef790d401999d))
* **Link:** prevent `type` bind on `<a>` ([b0df864](https://github.com/nuxt/ui/commit/b0df86437902696b594e5e7042601506a8bf4436))
* **SelectMenu:** take `option-attribute` into account to display label ([b9fe74b](https://github.com/nuxt/ui/commit/b9fe74bca5f48555e76c16237c2acc868f69e243)), closes [#1151](https://github.com/nuxt/ui/issues/1151)
* **Tooltip:** typo in kbd component ([4405d32](https://github.com/nuxt/ui/commit/4405d3239f7e19d399659347f079555318b3231b))
## [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) ## [2.11.1](https://github.com/nuxt/ui/compare/v2.11.0...v2.11.1) (2023-12-11)

View File

@@ -44,10 +44,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types' import type { NavItem } from '@nuxt/content/dist/runtime/types'
import type { HeaderLink } from '#ui-pro/types' import type { Link } from '#ui-pro/types'
defineProps<{ defineProps<{
links: HeaderLink[] links: Link[]
}>() }>()
const route = useRoute() const route = useRoute()

View File

@@ -217,7 +217,6 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
const code = computed(() => { const code = computed(() => {
let code = `\`\`\`html let code = `\`\`\`html
<template>
<${name}` <${name}`
for (const [key, value] of Object.entries(fullProps.value)) { for (const [key, value] of Object.entries(fullProps.value)) {
if (value === 'undefined' || value === null) { if (value === 'undefined' || value === null) {
@@ -247,7 +246,7 @@ const code = computed(() => {
} else { } else {
code += ' />' code += ' />'
} }
code += `\n</template> code += `
\`\`\` \`\`\`
` `
return code return code

View File

@@ -17,7 +17,7 @@ const form = reactive({ email: 'mail@example.com', password: 'password' })
<UButton label="Login" color="gray" block /> <UButton label="Login" color="gray" block />
</div> </div>
<UDivider label="OR" orientation="vertical" /> <UDivider label="OR" color="gray" orientation="vertical" />
<div class="space-y-4 flex flex-col justify-center"> <div class="space-y-4 flex flex-col justify-center">
<UButton color="black" label="Login with GitHub" icon="i-simple-icons-github" block /> <UButton color="black" label="Login with GitHub" icon="i-simple-icons-github" block />
@@ -37,7 +37,7 @@ const form = reactive({ email: 'mail@example.com', password: 'password' })
<UButton label="Login" color="gray" block /> <UButton label="Login" color="gray" block />
<UDivider label="OR" /> <UDivider label="OR" color="gray" />
<UButton color="black" label="Login with GitHub" icon="i-simple-icons-github" block /> <UButton color="black" label="Login with GitHub" icon="i-simple-icons-github" block />
<UButton color="black" label="Login with Google" icon="i-simple-icons-google" block /> <UButton color="black" label="Login with Google" icon="i-simple-icons-google" block />

View File

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

View File

@@ -13,14 +13,14 @@ const people = [{
name: 'Tom Cook' name: 'Tom Cook'
}] }]
const selected = ref(people[0].id) const selected = ref(people[0].name)
</script> </script>
<template> <template>
<UInputMenu <UInputMenu
v-model="selected" v-model="selected"
:options="people" :options="people"
value-attribute="id" value-attribute="name"
option-attribute="name" option-attribute="name"
/> />
</template> </template>

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
<template> <template>
<UPopover overlay> <div class="flex gap-4 items-center">
<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> <template #panel>
<div class="p-4"> <div class="p-4">
<Placeholder class="h-20 w-48" /> <Placeholder class="h-20 w-48" />
</div> </div>
</template> </template>
</UPopover> </UPopover>
</div>
</template> </template>

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ const people = [{
name: 'Tom Cook' name: 'Tom Cook'
}] }]
const selected = ref(people[0].id) const selected = ref(people[0].name)
</script> </script>
<template> <template>
@@ -21,7 +21,7 @@ const selected = ref(people[0].id)
v-model="selected" v-model="selected"
:options="people" :options="people"
placeholder="Select people" placeholder="Select people"
value-attribute="id" value-attribute="name"
option-attribute="name" option-attribute="name"
/> />
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ onMounted(() => {
/> />
</UAvatarGroup> </UAvatarGroup>
<UButton label="Button" loading /> <UButton label="Button" icon="i-heroicons-pencil-square" />
<UBadge label="Badge" /> <UBadge label="Badge" />

View File

@@ -20,10 +20,6 @@ yarn add @nuxt/ui
npm install @nuxt/ui npm install @nuxt/ui
``` ```
```bash [bun]
bun add @nuxt/ui
```
:: ::
2. Add it to your `modules` section in your `nuxt.config`: 2. Add it to your `modules` section in your `nuxt.config`:
@@ -36,114 +32,8 @@ export default defineNuxtConfig({
That's it! You can now use all the components and composables in your Nuxt app ✨ 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"} ::callout{icon="i-heroicons-exclamation-triangle"}
You should remove them from your `modules` and `dependencies` if you've previously installed them. 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.
::
### `@nuxtjs/tailwindcss`
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 ## IntelliSense

View File

@@ -281,14 +281,12 @@ export default defineNuxtConfig({
Search the icon you want to use on https://icones.js.org built by [@antfu](https://github.com/antfu). Search the icon you want to use on https://icones.js.org built by [@antfu](https://github.com/antfu).
:: ::
Thanks to [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons) plugin, only the icons you use in your app will be bundled in your CSS. However, you need to install the icon collections you specified in the `ui.icons` key: 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.
::code-group ::code-group
```bash [pnpm]
pnpm i @iconify-json/{collection_name}
```
```bash [yarn] ```bash [yarn]
yarn add @iconify-json/{collection_name} yarn add @iconify-json/{collection_name}
``` ```
@@ -297,21 +295,25 @@ yarn add @iconify-json/{collection_name}
npm install @iconify-json/{collection_name} npm install @iconify-json/{collection_name}
``` ```
```sh [pnpm]
pnpm i @iconify-json/{collection_name}
```
:: ::
If you choose to use the full `@iconify/json` icon collection (50MB), you can specifiy `icons: 'all'` or `icons: {}` in your `nuxt.config.ts` to use any icon in your app. When using `@iconify/json`, you can specifiy `icons: 'all'` in your `nuxt.config.ts` to use any icon in your app.
```ts [nuxt.config.ts] ```ts [nuxt.config.ts]
export default defineNuxtConfig({ export default defineNuxtConfig({
ui: { ui: {
icons: {} icons: 'all'
} }
}) })
``` ```
### Custom config ### 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] ```ts [nuxt.config.ts]
import { getIconCollections } from '@egoist/tailwindcss-icons' import { getIconCollections } from '@egoist/tailwindcss-icons'
@@ -343,13 +345,7 @@ 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`. You can easily replace all the default icons of the components in your `app.config.ts`.

View File

@@ -42,12 +42,6 @@ props:
variant: 'soft' variant: 'soft'
size: 'sm' size: 'sm'
options: options:
- name: color
restriction: included
values:
- gray
- white
- black
- name: variant - name: variant
restriction: included restriction: included
values: values:

View File

@@ -21,7 +21,6 @@ Pass an array of arrays to the `items` prop of the Dropdown component. Each arra
- `shortcuts` - The shortcuts of the item. - `shortcuts` - The shortcuts of the item.
- `slot` - The slot of the item. - `slot` - The slot of the item.
- `disabled` - Whether the item is disabled. - `disabled` - Whether the item is disabled.
- `class` - The class of the item.
- `click` - The click handler 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. You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
@@ -34,12 +33,6 @@ Use the `mode` prop to switch between `click` and `hover` modes.
:component-example{component="dropdown-example-mode"} :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 ## Popper
Use the `popper` prop to customize the popper instance. Use the `popper` prop to customize the popper instance.

View File

@@ -32,7 +32,3 @@ Link
It also renders an `<a>` tag when a `to` prop is provided, otherwise it defaults to rendering a `<button>` tag. The default behavior can be customized using the `as` prop. It also renders an `<a>` tag when a `to` prop is provided, otherwise it defaults to rendering a `<button>` tag. The default behavior can be customized using the `as` prop.
It is used underneath by the [Button](/elements/button), [Dropdown](/elements/dropdown) and [VerticalNavigation](/navigation/vertical-navigation) components. It is used underneath by the [Button](/elements/button), [Dropdown](/elements/dropdown) and [VerticalNavigation](/navigation/vertical-navigation) components.
## Props
:component-props

View File

@@ -203,9 +203,6 @@ componentProps:
## API ## API
::field-group ::field-group
::field{name="submit ()" type="Promise<void>"}
Triggers form submission.
::
::field{name="validate (path?: string, opts: { silent?: boolean })" type="Promise<T>"} ::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. Triggers form validation. Will raise any errors unless `opts.silent` is set to true.
:: ::

View File

@@ -100,46 +100,6 @@ excludedProps:
Learn how to customize icons from the [Input](/forms/input#icon) component. 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 ## Popper
Use the `popper` prop to customize the popper instance. Use the `popper` prop to customize the popper instance.

View File

@@ -117,8 +117,6 @@ 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. 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 ::component-card
--- ---
baseProps: baseProps:
@@ -132,18 +130,6 @@ 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 ### 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. 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.
@@ -152,7 +138,7 @@ Use the `debounce` prop to adjust the delay of the function.
::component-example ::component-example
--- ---
component: 'select-menu-example-search-async' component: 'select-menu-example-async-search'
componentProps: componentProps:
class: 'w-full lg:w-48' class: 'w-full lg:w-48'
--- ---
@@ -172,20 +158,6 @@ 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 ## Popper
Use the `popper` prop to customize the popper instance. Use the `popper` prop to customize the popper instance.

View File

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

View File

@@ -6,10 +6,6 @@ links:
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/data/Table.vue 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 ## 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. 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.
@@ -32,7 +28,6 @@ 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`. - `sortable` - Whether the column is sortable. Defaults to `false`.
- `direction` - The sort direction to use on first click. Defaults to `asc`. - `direction` - The sort direction to use on first click. Defaults to `asc`.
- `class` - The class to apply to the column cells. - `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"} ::component-example{class="grid"}
--- ---
@@ -58,8 +53,6 @@ componentProps:
You can make the columns sortable by setting the `sortable` property to `true` in the column configuration. 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"} ::component-example{class="grid"}
--- ---
padding: false padding: false
@@ -69,84 +62,17 @@ componentProps:
--- ---
:: ::
#### Default sorting 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`.
You can specify a default sort for the table through the `sort` prop. It's an object with the following properties: 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. - `column` - The column to sort by.
- `direction` - The sort direction. Can be either `asc` or `desc` and defaults to `asc`. - `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`. 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`. 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"} ::component-card{class="grid"}
@@ -225,6 +151,43 @@ 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. 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 ### Selectable
Use a `v-model` to make the table selectable. The `v-model` will be an array of the selected rows. Use a `v-model` to make the table selectable. The `v-model` will be an array of the selected rows.

View File

@@ -27,12 +27,6 @@ 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. 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 ## Slots
You can use slots to customize links display. You can use slots to customize links display.

View File

@@ -46,22 +46,6 @@ 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 ### Active / Inactive
Use the `active-button` and `inactive-button` props to customize the active and inactive buttons of the Pagination. Use the `active-button` and `inactive-button` props to customize the active and inactive buttons of the Pagination.

View File

@@ -21,7 +21,7 @@ Use the `mode` prop to switch between `click` and `hover` modes.
### Manual ### Manual
Use a `v-model:open` to manually control the state. In this example, press :shortcut{value="O"} to toggle the popover. Use the `open` prop to manually control showing the panel.
:component-example{component="popover-example-open"} :component-example{component="popover-example-open"}
@@ -29,6 +29,10 @@ Use a `v-model:open` to manually control the state. In this example, press :shor
:component-example{component="popover-example-overlay"} :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 ## Popper
Use the `popper` prop to customize the popper instance. Use the `popper` prop to customize the popper instance.

View File

@@ -133,7 +133,7 @@ excludedProps:
### Timeout ### 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. Use the `timeout` prop to configure how long the Notification will remain. 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. You will see a progress bar at the bottom of the Notification which will indicate the remaining time. When hovering the Notification, the progress bar will be paused.

View File

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

View File

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

View File

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

View File

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

5409
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,10 +22,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, computed, toRef, watch } from 'vue' import { defineComponent, ref, computed, toRef, watch } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { AvatarSize, AvatarChipColor, AvatarChipPosition, Strategy } from '../../types' import type { AvatarSize, AvatarChipColor, AvatarChipPosition, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -1,9 +1,9 @@
import { h, cloneVNode, computed, toRef, defineComponent } from 'vue' import { h, cloneVNode, computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UAvatar from './Avatar.vue' import UAvatar from './Avatar.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig, getSlotsChildren } from '../../utils' import { mergeConfig, twMerge, getSlotsChildren } from '../../utils'
import type { AvatarSize, Strategy } from '../../types' import type { AvatarSize, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -7,9 +7,9 @@
<script lang="ts"> <script lang="ts">
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup' import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { BadgeColor, BadgeSize, BadgeVariant, Strategy } from '../../types' import type { BadgeColor, BadgeSize, BadgeVariant, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error

View File

@@ -1,5 +1,5 @@
<template> <template>
<ULink :type="type" :disabled="disabled || loading" :class="buttonClass" v-bind="{ ...linkProps, ...attrs }"> <ULink :type="type" :disabled="disabled || loading" :class="buttonClass" v-bind="attrs">
<slot name="leading" :disabled="disabled" :loading="loading"> <slot name="leading" :disabled="disabled" :loading="loading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" /> <UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
</slot> </slot>
@@ -19,11 +19,11 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, toRef } from 'vue' import { computed, defineComponent, toRef } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import ULink from '../elements/Link.vue' import ULink from '../elements/Link.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig, nuxtLinkProps, getNuxtLinkProps } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup' import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { ButtonColor, ButtonSize, ButtonVariant, Strategy } from '../../types' import type { ButtonColor, ButtonSize, ButtonVariant, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
@@ -39,7 +39,6 @@ export default defineComponent({
}, },
inheritAttrs: false, inheritAttrs: false,
props: { props: {
...nuxtLinkProps,
type: { type: {
type: String, type: String,
default: 'button' default: 'button'
@@ -191,8 +190,6 @@ export default defineComponent({
) )
}) })
const linkProps = computed(() => getNuxtLinkProps(props))
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
@@ -204,8 +201,7 @@ export default defineComponent({
leadingIconName, leadingIconName,
trailingIconName, trailingIconName,
leadingIconClass, leadingIconClass,
trailingIconClass, trailingIconClass
linkProps
} }
} }
}) })

View File

@@ -1,8 +1,8 @@
import { h, computed, toRef, defineComponent } from 'vue' import { h, computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig, getSlotsChildren } from '../../utils' import { mergeConfig, twMerge, getSlotsChildren } from '../../utils'
import { useProvideButtonGroup } from '../../composables/useButtonGroup' import { useProvideButtonGroup } from '../../composables/useButtonGroup'
import type { ButtonSize, Strategy } from '../../types' import type { ButtonSize, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error

View File

@@ -1,5 +1,4 @@
<template> <template>
<!-- eslint-disable-next-line vue/no-template-shadow -->
<HMenu v-slot="{ open }" as="div" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave"> <HMenu v-slot="{ open }" as="div" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave">
<HMenuButton <HMenuButton
ref="trigger" ref="trigger"
@@ -20,17 +19,16 @@
<Transition appear v-bind="ui.transition"> <Transition appear v-bind="ui.transition">
<div> <div>
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(ui.arrow)" /> <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> <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"> <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="getNuxtLinkProps(item)" 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', 'click'])" custom>
<HMenuItem v-slot="{ active, disabled: itemDisabled, close }" :disabled="item.disabled"> <HMenuItem v-slot="{ active, disabled: itemDisabled, close }" :disabled="item.disabled">
<component <component
:is="!!href ? 'a' : 'button'" :is="!!href ? 'a' : 'button'"
:href="!itemDisabled ? href : undefined" :href="!itemDisabled ? href : undefined"
:rel="rel" :rel="rel"
:target="target" :target="target"
: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)" :class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]"
@click="onClick($event, item, { href, navigate, close, isExternal })" @click="onClick($event, item, { href, navigate, close, isExternal })"
> >
<slot :name="item.slot || 'item'" :item="item"> <slot :name="item.slot || 'item'" :item="item">
@@ -55,17 +53,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, computed, watch, toRef, onMounted, resolveComponent } from 'vue' import { defineComponent, ref, computed, toRef, onMounted, resolveComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue' import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
import { defu } from 'defu' import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue' import UKbd from '../elements/Kbd.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper' import { usePopper } from '../../composables/usePopper'
import { mergeConfig, getNuxtLinkProps } from '../../utils' import { mergeConfig, twMerge, omit } from '../../utils'
import type { DropdownItem, PopperOptions, Strategy } from '../../types' import type { DropdownItem, PopperOptions, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
@@ -94,10 +92,6 @@ export default defineComponent({
default: 'click', default: 'click',
validator: (value: string) => ['click', 'hover'].includes(value) validator: (value: string) => ['click', 'hover'].includes(value)
}, },
open: {
type: Boolean,
default: undefined
},
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false default: false
@@ -123,8 +117,7 @@ export default defineComponent({
default: () => ({}) default: () => ({})
} }
}, },
emits: ['update:open'], setup (props) {
setup (props, { emit }) {
const { ui, attrs } = useUI('dropdown', toRef(props, 'ui'), config, toRef(props, 'class')) 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)) const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
@@ -138,46 +131,21 @@ export default defineComponent({
let closeTimeout: NodeJS.Timeout | null = null let closeTimeout: NodeJS.Timeout | null = null
onMounted(() => { onMounted(() => {
// @ts-expect-error internals setTimeout(() => {
const menuProvides = trigger.value?.$.provides // @ts-expect-error internals
if (!menuProvides) { const menuProvides = trigger.value?.$.provides
return if (!menuProvides) {
} return
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides) }
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]] const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
if (props.open) { }, 200)
menuApi.value?.openMenu()
}
}) })
const containerStyle = computed(() => { const containerStyle = computed(() => {
if (props.mode !== 'hover') {
return {}
}
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8 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 props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
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 () { function onMouseOver () {
@@ -232,23 +200,6 @@ 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') const NuxtLink = resolveComponent('NuxtLink')
return { return {
@@ -263,7 +214,7 @@ export default defineComponent({
onMouseOver, onMouseOver,
onMouseLeave, onMouseLeave,
onClick, onClick,
getNuxtLinkProps, omit,
twMerge, twMerge,
twJoin, twJoin,
NuxtLink NuxtLink

View File

@@ -7,9 +7,9 @@
<script lang="ts"> <script lang="ts">
import { toRef, defineComponent, computed } from 'vue' import { toRef, defineComponent, computed } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { KbdSize, Strategy } from '../../types' import type { KbdSize, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -2,7 +2,6 @@
<component <component
:is="as" :is="as"
v-if="!to" v-if="!to"
:type="type"
:disabled="disabled" :disabled="disabled"
v-bind="$attrs" v-bind="$attrs"
:class="active ? activeClass : inactiveClass" :class="active ? activeClass : inactiveClass"
@@ -33,20 +32,16 @@
<script lang="ts"> <script lang="ts">
import { isEqual } from 'ohash' import { isEqual } from 'ohash'
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { nuxtLinkProps } from '../../utils' import { NuxtLink } from '#components'
export default defineComponent({ export default defineComponent({
inheritAttrs: false, inheritAttrs: false,
props: { props: {
...nuxtLinkProps, ...NuxtLink.props,
as: { as: {
type: String, type: String,
default: 'button' default: 'button'
}, },
type: {
type: String,
default: 'button'
},
disabled: { disabled: {
type: Boolean, type: Boolean,
default: null default: null

View File

@@ -31,10 +31,10 @@
<script lang="ts"> <script lang="ts">
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup' import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { Strategy } from '../../types' import type { Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

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

View File

@@ -8,7 +8,7 @@
:type="type" :type="type"
:required="required" :required="required"
:placeholder="placeholder" :placeholder="placeholder"
:disabled="disabled" :disabled="disabled || loading"
:class="inputClass" :class="inputClass"
v-bind="attrs" v-bind="attrs"
@input="onInput" @input="onInput"
@@ -34,12 +34,12 @@
<script lang="ts"> <script lang="ts">
import { ref, computed, toRef, onMounted, defineComponent } from 'vue' import { ref, computed, toRef, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import { defu } from 'defu' import { defu } from 'defu'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup' import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig, looseToNumber } from '../../utils' import { mergeConfig, twMerge, looseToNumber } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup' import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { InputSize, InputColor, InputVariant, Strategy } from '../../types' import type { InputSize, InputColor, InputVariant, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error

View File

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

View File

@@ -28,9 +28,9 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, toRef, onMounted, ref } from 'vue' import { computed, defineComponent, inject, toRef, onMounted, ref } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { Strategy } from '../../types' import type { Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

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

View File

@@ -22,10 +22,10 @@
<script lang="ts"> <script lang="ts">
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup' import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { RangeSize, Strategy } from '../../types' import type { RangeSize, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -5,7 +5,7 @@
:name="name" :name="name"
:value="modelValue" :value="modelValue"
:required="required" :required="required"
:disabled="disabled" :disabled="disabled || loading"
:class="selectClass" :class="selectClass"
v-bind="attrs" v-bind="attrs"
@input="onInput" @input="onInput"
@@ -55,11 +55,11 @@
<script lang="ts"> <script lang="ts">
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType, ComputedRef } from 'vue' import type { PropType, ComputedRef } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup' import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig, get } from '../../utils' import { mergeConfig, twMerge, get } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup' import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { SelectSize, SelectColor, SelectVariant, Strategy } from '../../types' import type { SelectSize, SelectColor, SelectVariant, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error

View File

@@ -6,7 +6,7 @@
:name="name" :name="name"
:model-value="modelValue" :model-value="modelValue"
:multiple="multiple" :multiple="multiple"
:disabled="disabled" :disabled="disabled || loading"
as="div" as="div"
:class="ui.wrapper" :class="ui.wrapper"
@update:model-value="onUpdate" @update:model-value="onUpdate"
@@ -28,7 +28,7 @@
:class="uiMenu.trigger" :class="uiMenu.trigger"
> >
<slot :open="open" :disabled="disabled" :loading="loading"> <slot :open="open" :disabled="disabled" :loading="loading">
<button :id="inputId" :class="selectClass" :disabled="disabled" type="button" v-bind="attrs"> <button :id="inputId" :class="selectClass" :disabled="disabled || loading" type="button" v-bind="attrs">
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass"> <span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
<slot name="leading" :disabled="disabled" :loading="loading"> <slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" /> <UIcon :name="leadingIconName" :class="leadingIconClass" />
@@ -36,7 +36,8 @@
</span> </span>
<slot name="label"> <slot name="label">
<span v-if="label" :class="uiMenu.label">{{ label }}</span> <span v-if="multiple && Array.isArray(modelValue) && modelValue.length" :class="uiMenu.label">{{ modelValue.length }} selected</span>
<span v-else-if="!multiple && modelValue" :class="uiMenu.label">{{ ['string', 'number'].includes(typeof modelValue) ? modelValue : modelValue[optionAttribute] }}</span>
<span v-else :class="uiMenu.label">{{ placeholder || '&nbsp;' }}</span> <span v-else :class="uiMenu.label">{{ placeholder || '&nbsp;' }}</span>
</slot> </slot>
@@ -57,13 +58,14 @@
<component :is="searchable ? 'HComboboxOptions' : 'HListboxOptions'" static :class="[uiMenu.base, uiMenu.ring, uiMenu.rounded, uiMenu.shadow, uiMenu.background, uiMenu.padding, uiMenu.height]"> <component :is="searchable ? 'HComboboxOptions' : 'HListboxOptions'" static :class="[uiMenu.base, uiMenu.ring, uiMenu.rounded, uiMenu.shadow, uiMenu.background, uiMenu.padding, uiMenu.height]">
<HComboboxInput <HComboboxInput
v-if="searchable" v-if="searchable"
ref="searchInput"
:display-value="() => query" :display-value="() => query"
name="q" name="q"
:placeholder="searchablePlaceholder" :placeholder="searchablePlaceholder"
autofocus autofocus
autocomplete="off" autocomplete="off"
:class="uiMenu.input" :class="uiMenu.input"
@change="onChange" @change="query = $event.target.value"
/> />
<component <component
:is="searchable ? 'HComboboxOption' : 'HListboxOption'" :is="searchable ? 'HComboboxOption' : 'HListboxOption'"
@@ -96,11 +98,11 @@
</li> </li>
</component> </component>
<component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && createOption" v-slot="{ active, selected }" :value="createOption" as="template"> <component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" 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]"> <li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive]">
<div :class="uiMenu.option.container"> <div :class="uiMenu.option.container">
<slot name="option-create" :option="createOption" :active="active" :selected="selected"> <slot name="option-create" :option="queryOption" :active="active" :selected="selected">
<span :class="uiMenu.option.create">Create "{{ createOption[optionAttribute] }}"</span> <span :class="uiMenu.option.create">Create "{{ queryOption[optionAttribute] }}"</span>
</slot> </slot>
</div> </div>
</li> </li>
@@ -124,7 +126,7 @@
<script lang="ts"> <script lang="ts">
import { ref, computed, toRef, watch, defineComponent } from 'vue' import { ref, computed, toRef, watch, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType, ComponentPublicInstance } from 'vue'
import { import {
Combobox as HCombobox, Combobox as HCombobox,
ComboboxButton as HComboboxButton, ComboboxButton as HComboboxButton,
@@ -138,13 +140,13 @@ import {
} from '@headlessui/vue' } from '@headlessui/vue'
import { computedAsync, useDebounceFn } from '@vueuse/core' import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu' import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper' import { usePopper } from '../../composables/usePopper'
import { useFormGroup } from '../../composables/useFormGroup' import { useFormGroup } from '../../composables/useFormGroup'
import { get, mergeConfig } from '../../utils' import { get, mergeConfig, twMerge } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup' import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { SelectSize, SelectColor, SelectVariant, PopperOptions, Strategy } from '../../types' import type { SelectSize, SelectColor, SelectVariant, PopperOptions, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
@@ -175,10 +177,6 @@ export default defineComponent({
type: [String, Number, Object, Array], type: [String, Number, Object, Array],
default: '' default: ''
}, },
query: {
type: String,
default: null
},
by: { by: {
type: String, type: String,
default: undefined default: undefined
@@ -249,7 +247,7 @@ export default defineComponent({
}, },
clearSearchOnClose: { clearSearchOnClose: {
type: Boolean, type: Boolean,
default: () => configMenu.default.clearSearchOnClose default: () => configMenu.default.clearOnClose
}, },
debounce: { debounce: {
type: Number, type: Number,
@@ -259,10 +257,6 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
showCreateOptionWhen: {
type: String as PropType<'always' | 'empty'>,
default: () => configMenu.default.showCreateOptionWhen
},
placeholder: { placeholder: {
type: String, type: String,
default: null default: null
@@ -328,7 +322,7 @@ export default defineComponent({
default: () => ({}) default: () => ({})
} }
}, },
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change'], emits: ['update:modelValue', 'open', 'close', 'change'],
setup (props, { emit, slots }) { setup (props, { emit, slots }) {
const { ui, attrs } = useUI('select', toRef(props, 'ui'), config, toRef(props, 'class')) const { ui, attrs } = useUI('select', toRef(props, 'ui'), config, toRef(props, 'class'))
@@ -343,33 +337,8 @@ export default defineComponent({
const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value) const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value)
const internalQuery = ref('') const query = ref('')
const query = computed({ const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
get () {
return props.query ?? internalQuery.value
},
set (value) {
internalQuery.value = value
emit('update:query', value)
}
})
const label = computed(() => {
if (props.multiple) {
if (Array.isArray(props.modelValue) && props.modelValue.length) {
return `${props.modelValue.length} selected`
} else {
return null
}
} else {
if (props.valueAttribute) {
const option = props.options.find(option => option[props.valueAttribute] === props.modelValue)
return option ? option[props.optionAttribute] : null
} else {
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : props.modelValue[props.optionAttribute]
}
}
})
const selectClass = computed(() => { const selectClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant] const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -469,21 +438,8 @@ export default defineComponent({
}) })
}) })
const createOption = computed(() => { const queryOption = computed(() => {
if (query.value === '') { return query.value === '' ? null : { [props.optionAttribute]: 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 () { function clearOnClose () {
@@ -503,15 +459,17 @@ export default defineComponent({
}) })
function onUpdate (event: any) { 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('update:modelValue', event)
emit('change', event) emit('change', event)
emitFormChange() emitFormChange()
} }
function onChange (event: any) {
query.value = event.target.value
}
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
@@ -525,7 +483,6 @@ export default defineComponent({
popper, popper,
trigger, trigger,
container, container,
label,
isLeading, isLeading,
isTrailing, isTrailing,
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
@@ -537,11 +494,9 @@ export default defineComponent({
trailingIconClass, trailingIconClass,
trailingWrapperIconClass, trailingWrapperIconClass,
filteredOptions, filteredOptions,
createOption, queryOption,
// eslint-disable-next-line vue/no-dupe-keys
query, query,
onUpdate, onUpdate
onChange
} }
} }
}) })

View File

@@ -23,11 +23,11 @@
<script lang="ts"> <script lang="ts">
import { ref, computed, toRef, watch, onMounted, nextTick, defineComponent } from 'vue' import { ref, computed, toRef, watch, onMounted, nextTick, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { defu } from 'defu' import { defu } from 'defu'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup' import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig, looseToNumber } from '../../utils' import { mergeConfig, twMerge, looseToNumber } from '../../utils'
import type { TextareaSize, TextareaColor, TextareaVariant, Strategy } from '../../types' import type { TextareaSize, TextareaColor, TextareaVariant, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -22,11 +22,11 @@
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { Switch as HSwitch } from '@headlessui/vue' import { Switch as HSwitch } from '@headlessui/vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup' import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { ToggleSize, Strategy } from '../../types' import type { ToggleSize, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
@@ -89,7 +89,7 @@ export default defineComponent({
default: () => ({}) default: () => ({})
} }
}, },
emits: ['update:modelValue', 'change'], emits: ['update:modelValue'],
setup (props, { emit }) { setup (props, { emit }) {
const { ui, attrs } = useUI('toggle', toRef(props, 'ui'), config) const { ui, attrs } = useUI('toggle', toRef(props, 'ui'), config)

View File

@@ -19,9 +19,9 @@
<script lang="ts"> <script lang="ts">
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { Strategy } from '../../types' import type { Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -7,9 +7,9 @@
<script lang="ts"> <script lang="ts">
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { Strategy } from '../../types' import type { Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -21,11 +21,11 @@
<script lang="ts"> <script lang="ts">
import { toRef, computed, defineComponent } from 'vue' import { toRef, computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { Avatar, Strategy } from '../../types' import type { Avatar, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
@@ -74,25 +74,27 @@ export default defineComponent({
setup (props) { setup (props) {
const { ui, attrs } = useUI('divider', toRef(props, 'ui'), config) const { ui, attrs } = useUI('divider', toRef(props, 'ui'), config)
const isHorizontal = computed(() => props.orientation === 'horizontal' )
const wrapperClass = computed(() => { const wrapperClass = computed(() => {
return twMerge(twJoin( return twMerge(twJoin(
ui.value.wrapper.base, ui.value.wrapper.base,
ui.value.wrapper[props.orientation] isHorizontal.value ? ui.value.wrapper.horizontal : ui.value.wrapper.vertical
), props.class) ), props.class)
}) })
const containerClass = computed(() => { const containerClass = computed(() => {
return twJoin( return twJoin(
ui.value.container.base, ui.value.container.base,
ui.value.container[props.orientation] isHorizontal.value ? ui.value.container.horizontal : ui.value.container.vertical
) )
}) })
const borderClass = computed(() => { const borderClass = computed(() => {
return twJoin( return twJoin(
ui.value.border.base, ui.value.border.base,
ui.value.border[props.orientation], isHorizontal.value ? ui.value.border.horizontal : ui.value.border.vertical,
ui.value.border.size[props.orientation], isHorizontal.value ? ui.value.border.size.horizontal : ui.value.border.size.vertical,
ui.value.border.type[props.type] ui.value.border.type[props.type]
) )
}) })

View File

@@ -5,9 +5,9 @@
<script lang="ts"> <script lang="ts">
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { Strategy } from '../../types' import type { Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -5,7 +5,7 @@
<ULink <ULink
as="span" as="span"
:class="[ui.base, index === links.length - 1 ? ui.active : !!link.to ? ui.inactive : '']" :class="[ui.base, index === links.length - 1 ? ui.active : !!link.to ? ui.inactive : '']"
v-bind="getULinkProps(link)" v-bind="omit(link, ['label', 'labelClass', 'icon', 'iconClass'])"
:aria-current="index === links.length - 1 ? 'page' : undefined" :aria-current="index === links.length - 1 ? 'page' : undefined"
> >
<slot name="icon" :link="link" :index="index" :is-active="index === links.length - 1"> <slot name="icon" :link="link" :index="index" :is-active="index === links.length - 1">
@@ -35,11 +35,11 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, toRef } from 'vue' import { defineComponent, toRef } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import ULink from '../elements/Link.vue' import ULink from '../elements/Link.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig, getULinkProps } from '../../utils' import { mergeConfig, twMerge, omit } from '../../utils'
import type { BreadcrumbLink, Strategy } from '../../types' import type { BreadcrumbLink, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
@@ -78,7 +78,7 @@ export default defineComponent({
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
attrs, attrs,
getULinkProps, omit,
twMerge, twMerge,
twJoin twJoin
} }

View File

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

View File

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

View File

@@ -18,10 +18,10 @@ import type { PropType, Ref } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import type { VirtualElement } from '@popperjs/core' import type { VirtualElement } from '@popperjs/core'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper' import { usePopper } from '../../composables/usePopper'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { PopperOptions, Strategy } from '../../types' import type { PopperOptions, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -45,13 +45,13 @@
<script lang="ts"> <script lang="ts">
import { ref, computed, toRef, onMounted, onUnmounted, watchEffect, defineComponent } from 'vue' import { ref, computed, toRef, onMounted, onUnmounted, watchEffect, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useTimer } from '../../composables/useTimer' import { useTimer } from '../../composables/useTimer'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { Avatar, Button, NotificationColor, NotificationAction, Strategy } from '../../types' import type { Avatar, Button, NotificationColor, NotificationAction, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'

View File

@@ -22,11 +22,11 @@
<script lang="ts"> <script lang="ts">
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twJoin } from 'tailwind-merge'
import UNotification from './Notification.vue' import UNotification from './Notification.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { useToast } from '../../composables/useToast' import { useToast } from '../../composables/useToast'
import { mergeConfig } from '../../utils' import { mergeConfig, twMerge } from '../../utils'
import type { Notification, Strategy } from '../../types' import type { Notification, Strategy } from '../../types'
import { useState } from '#imports' import { useState } from '#imports'
// @ts-expect-error // @ts-expect-error

View File

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

View File

@@ -19,7 +19,7 @@
<UKbd v-for="shortcut of shortcuts" :key="shortcut" size="xs"> <UKbd v-for="shortcut of shortcuts" :key="shortcut" size="xs">
{{ shortcut }} {{ shortcut }}
</UKbd> </Ukbd>
</span> </span>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@@ -6,10 +6,6 @@ export type ChipColor = 'gray' | typeof colors[number]
export type ChipPosition = keyof typeof chip.position export type ChipPosition = keyof typeof chip.position
export interface Chip { export interface Chip {
size?: ChipSize
color?: ChipColor color?: ChipColor
position?: ChipPosition position?: ChipPosition
text?: string
inset?: boolean
show?: boolean
} }

View File

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

View File

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

View File

@@ -22,6 +22,5 @@ export * from './select'
export * from './tabs' export * from './tabs'
export * from './textarea' export * from './textarea'
export * from './toggle' export * from './toggle'
export * from './tooltip'
export * from './vertical-navigation' export * from './vertical-navigation'
export * from './utils' export * from './utils'

View File

@@ -1,9 +1,6 @@
import type { NuxtLinkProps } from '#app' import type { NuxtLinkProps } from '#app'
export interface Link extends NuxtLinkProps { export interface Link extends NuxtLinkProps {
as?: string
type?: string
disabled?: boolean
active?: boolean active?: boolean
exact?: boolean exact?: boolean
exactQuery?: boolean exactQuery?: boolean

View File

@@ -1,7 +0,0 @@
export interface Tooltip {
text?: string
prevent?: boolean
shortcuts?: string[]
openDelay?: number
closeDelay?: number
}

View File

@@ -4,7 +4,7 @@ import inputMenu from './inputMenu'
export default { export default {
...inputMenu, ...inputMenu,
select: 'inline-flex items-center text-left cursor-default', 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 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 focus:border-inherit 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', required: 'absolute inset-0 w-px opacity-0 cursor-default',
label: 'block truncate', label: 'block truncate',
option: { option: {
@@ -22,8 +22,7 @@ export default {
}, },
default: { default: {
selectedIcon: 'i-heroicons-check-20-solid', selectedIcon: 'i-heroicons-check-20-solid',
clearSearchOnClose: false, clearOnClose: false
showCreateOptionWhen: 'empty'
}, },
arrow: { arrow: {
...arrow, ...arrow,

View File

@@ -1,5 +1,5 @@
export default { export default {
base: '', base: 'overflow-hidden',
background: 'bg-white dark:bg-gray-900', background: 'bg-white dark:bg-gray-900',
divide: 'divide-y divide-gray-200 dark:divide-gray-800', divide: 'divide-y divide-gray-200 dark:divide-gray-800',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800', ring: 'ring-1 ring-gray-200 dark:ring-gray-800',

View File

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

View File

@@ -1,25 +1,21 @@
import { defu, createDefu } from 'defu' import { defu, createDefu } from 'defu'
import { extendTailwindMerge } from 'tailwind-merge' import { extendTailwindMerge } from 'tailwind-merge'
import type { Strategy } from '../types' import type { Strategy } from '../types'
// @ts-ignore
import appConfig from '#build/app.config'
const customTwMerge = extendTailwindMerge<string, string>({ export const twMerge = extendTailwindMerge<string, string>(defu({
extend: { extend: {
classGroups: { classGroups: {
icons: [(classPart: string) => /^i-/.test(classPart)] icons: [(classPart: string) => /^i-/.test(classPart)]
} }
} }
}) }, appConfig.ui?.tailwindMerge))
const defuTwMerge = createDefu((obj, key, value, namespace) => { const defuTwMerge = createDefu((obj, key, value, namespace) => {
if (namespace === 'default' || namespace.startsWith('default.')) { if (namespace !== 'default' && !namespace.startsWith('default.') && typeof obj[key] === 'string' && typeof value === 'string' && obj[key] && value) {
return false
}
if (namespace.endsWith('avatar') && key === 'size') {
return false
}
if (typeof obj[key] === 'string' && typeof value === 'string' && obj[key] && value) {
// @ts-ignore // @ts-ignore
obj[key] = customTwMerge(obj[key], value) obj[key] = twMerge(obj[key], value)
return true return true
} }
}) })
@@ -74,4 +70,3 @@ export function looseToNumber (val: any): any {
} }
export * from './lodash' export * from './lodash'
export * from './link'

View File

@@ -1,119 +0,0 @@
import type { PropType } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
export const nuxtLinkProps = {
to: {
type: [String, Object] as PropType<RouteLocationRaw>,
default: undefined
},
href: {
type: [String, Object] as PropType<RouteLocationRaw>,
default: undefined
},
// Attributes
target: {
type: String as PropType<'_blank' | '_parent' | '_self' | '_top' | (string & {}) | null>,
default: undefined
},
rel: {
type: String as PropType<string | null>,
default: undefined
},
noRel: {
type: Boolean,
default: undefined
},
// Prefetching
prefetch: {
type: Boolean,
default: undefined
},
noPrefetch: {
type: Boolean,
default: undefined
},
// Styling
activeClass: {
type: String,
default: undefined
},
exactActiveClass: {
type: String,
default: undefined
},
prefetchedClass: {
type: String,
default: undefined
},
// Vue Router's `<RouterLink>` additional props
replace: {
type: Boolean,
default: undefined
},
ariaCurrentValue: {
type: String,
default: undefined
},
// Edge cases handling
external: {
type: Boolean,
default: undefined
}
} as const
const uLinkProps = {
as: {
type: String,
default: 'button'
},
type: {
type: String,
default: 'button'
},
disabled: {
type: Boolean,
default: null
},
active: {
type: Boolean,
default: undefined
},
exact: {
type: Boolean,
default: false
},
exactQuery: {
type: Boolean,
default: false
},
exactHash: {
type: Boolean,
default: false
},
inactiveClass: {
type: String,
default: undefined
}
} as const
export const getNuxtLinkProps = (props) => {
const keys = Object.keys(nuxtLinkProps)
return keys.reduce((acc, key) => {
if (props[key] !== undefined) {
acc[key] = props[key]
}
return acc
}, {})
}
export const getULinkProps = (props) => {
const keys = [...Object.keys(nuxtLinkProps), ...Object.keys(uLinkProps)]
return keys.reduce((acc, key) => {
if (props[key] !== undefined) {
acc[key] = props[key]
}
return acc
}, {})
}

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
// @vitest-environment nuxt
import { describe, it, expect } from 'vitest' import { describe, it, expect } from 'vitest'
import { UButton } from '#components' import Button from '../../src/runtime/components/elements/Button.vue'
import type { TypeOf } from 'zod' import type { TypeOf } from 'zod'
import ComponentRender from '../component-render' import ComponentRender from '../component-render'
type ButtonOptions = TypeOf<typeof Button.props>
describe('Button', () => { describe('Button', () => {
it.each([ it.each([
[ 'basic case', { } ], [ 'basic case', { } ],
@@ -11,12 +14,12 @@ describe('Button', () => {
[ 'rounded full', { props: { ui: { rounded: 'rounded-full' } } } ], [ 'rounded full', { props: { ui: { rounded: 'rounded-full' } } } ],
[ '<UButton icon="i-heroicons-pencil-square" size="sm" color="primary" square variant="solid" />' ] [ '<UButton icon="i-heroicons-pencil-square" size="sm" color="primary" square variant="solid" />' ]
// @ts-ignore // @ts-ignore
])('renders %s correctly', async (nameOrHtml: string, options: TypeOf<typeof Button.props>) => { ])('renders %s correctly', async (nameOrHtml: string, options: ButtonOptions) => {
if (options !== undefined) { if (options !== undefined) {
options.slots = options.slots || { default: () => 'label' } options.slots = options.slots || { default: () => 'label' }
options.slots.default = options.slots.default || (() => 'label') options.slots.default = options.slots.default || (() => 'label')
} }
const html = await ComponentRender(nameOrHtml, options, UButton) const html = await ComponentRender(nameOrHtml, options, Button)
expect(html).toMatchSnapshot() expect(html).toMatchSnapshot()
}) })
}) })

View File

@@ -1,34 +1,34 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // 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`] = ` 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-->
<!--v-if--> <!--v-if-->
</button>" </button>"
`; `;
exports[`Button > renders basic case correctly 1`] = ` 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-->label
<!--v-if--> <!--v-if-->
</button>" </button>"
`; `;
exports[`Button > renders black solid correctly 1`] = ` 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-->label
<!--v-if--> <!--v-if-->
</button>" </button>"
`; `;
exports[`Button > renders leading icon correctly 1`] = ` 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--> <!--v-if-->
</button>" </button>"
`; `;
exports[`Button > renders rounded full correctly 1`] = ` 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-->label
<!--v-if--> <!--v-if-->
</button>" </button>"

View File

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

View File

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

View File

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