Compare commits

..

22 Commits

Author SHA1 Message Date
Benjamin Canac
fd95a3f646 fix(module): handle tailwindMerge config from app.config 2023-12-15 14:55:28 +01:00
Benjamin Canac
23770d8cf0 chore(deps): update tailwind-merge 2023-12-15 14:42:57 +01:00
Benjamin Canac
84e75ad237 chore(playground): add tailwind.config.ts 2023-12-15 12:51:55 +01:00
Benjamin Canac
00dd8c27bd chore(playground): add app.config.ts 2023-12-15 12:50:58 +01:00
Benjamin Canac
5f81a79edf chore(Table): unused omit function 2023-12-15 11:37:08 +01:00
Benjamin Canac
1a02b3abe7 chore(Table): remove unused click attribute in rows 2023-12-14 17:17:43 +01:00
Benjamin Canac
83631ccbca fix(Link): handle active override when value is false 2023-12-14 14:32:31 +01:00
Benjamin Canac
0f9b5d47a6 docs(index): update components count 2023-12-13 15:20:48 +01:00
Benjamin Canac
f623ec1130 feat(Breadcrumb): handle labelClass and merge iconClass 2023-12-12 19:12:01 +01:00
Benjamin Canac
a79f7c0a34 feat(VerticalNavigation): handle labelClass and merge iconClass 2023-12-12 19:11:12 +01:00
Benjamin Canac
1c9835d7f1 feat(Dropdown): handle labelClass and merge iconClass
Resolves #716
2023-12-12 19:07:46 +01:00
Benjamin Canac
6d8d82a265 feat(InputMenu): new component (#1095) 2023-12-12 18:45:04 +01:00
Benjamin Canac
66a80c7486 docs(SelectMenu): add missing New badge on empty slot 2023-12-12 18:19:44 +01:00
Benjamin Canac
0546c7922c docs(ComponentPropsField): improve codeblocks line-height 2023-12-12 18:10:16 +01:00
Benjamin Canac
eafe707c7d docs(ComponentPropsField): wrong line-height on code blocks 2023-12-12 17:02:41 +01:00
Benjamin Canac
5d1919a538 feat(SelectMenu): add empty slot when no options
Resolves #1089
2023-12-12 16:27:42 +01:00
Benjamin Canac
781365a5ed docs(SelectMenu): improve 2023-12-12 16:27:42 +01:00
Benjamin Canac
0129e2db40 docs(SelectMenu): remove duplicate config 2023-12-12 16:27:22 +01:00
Benjamin Canac
45b1a4bd32 docs(releases): display pulls on release day too 2023-12-11 17:22:42 +01:00
Conner Blanton
f32f578125 feat(Popover): open and close events (#1038) 2023-12-11 17:19:19 +01:00
Benjamin Canac
4b044866a5 docs: add ads 2023-12-11 16:50:00 +01:00
Benjamin Canac
9b768ec12b docs: bump @nuxt/ui-pro-edge 2023-12-11 12:47:12 +01:00
72 changed files with 1166 additions and 232 deletions

View File

@@ -0,0 +1,45 @@
<template>
<div ref="carbonads" class="carbon" />
</template>
<script setup lang="ts">
const carbonads = ref(null)
onMounted(() => {
if (carbonads.value) {
const script = document.createElement('script')
script.setAttribute('type', 'text/javascript')
script.setAttribute('src', 'https://cdn.carbonads.com/carbon.js?serve=CWYIVK3E&placement=uinuxtcom')
script.setAttribute('id', '_carbonads_js')
carbonads.value.appendChild(script)
}
})
</script>
<style lang="postcss">
.carbon > #carbonads {
@apply relative border border-gray-200 dark:border-gray-800 rounded-lg bg-white dark:bg-gray-800/50 hover:border-gray-300 dark:hover:border-gray-700 w-full transition-colors min-h-[220px];
&:hover {
.carbon-text {
@apply text-gray-700 dark:text-gray-200;
}
}
.carbon-img {
@apply flex justify-center p-2 w-full;
& > img {
@apply !max-w-full w-full rounded;
}
}
.carbon-text {
@apply flex px-2 text-sm text-gray-500 dark:text-gray-400 transition-colors text-center w-full;
}
.carbon-poweredby {
@apply block text-xs text-center text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 pt-1 pb-2 px-2 transition-colors;
}
}
</style>

View File

@@ -0,0 +1,27 @@
<template>
<div class="relative group/ad border border-gray-200 dark:border-gray-800 rounded-lg bg-white dark:bg-gray-800/50 hover:border-gray-300 dark:hover:border-gray-700 p-2 w-full transition-colors">
<NuxtLink to="/pro" class="focus:outline-none" tabindex="-1">
<span class="absolute inset-0" aria-hidden="true" />
</NuxtLink>
<UColorModeImage
light="/pro/illustrations/docs-light.svg"
dark="/pro/illustrations/docs-dark.svg"
alt="Nuxt UI Pro"
loading="lazy"
class="w-full"
/>
<div class="flex flex-col items-center mt-2 text-center">
<div class="inline-flex gap-1.5">
<Logo class="h-4 w-auto" />
<UBadge variant="subtle" size="xs" label="Pro" class="font-semibold" />
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 group-hover/ad:text-gray-700 dark:group-hover/ad:text-gray-200 mt-1 transition-colors">
The Building Blocks for Modern Web Apps.
</p>
</div>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<Field v-bind="prop">
<code v-if="prop.default">{{ prop.default }}</code>
<code v-if="prop.default" class="leading-6">{{ prop.default }}</code>
<p v-if="prop.description">
{{ prop.description }}
</p>
@@ -22,8 +22,8 @@
<ComponentPropsField v-for="subProp in Object.values(prop.schema.schema)" :key="(subProp as any).name" :prop="subProp" />
</FieldGroup>
</Collapsible>
<div v-else-if="prop.schema?.kind === 'enum' && prop.schema.type !== 'boolean' && startsWithCapital(prop.schema.type) && !prop.schema.type.startsWith(prop.schema.schema[0])" class="space-x-1 leading-7 -my-1">
<code v-for="value in prop.schema.schema.filter(value => typeof value === 'string')" :key="value" class="whitespace-pre-wrap break-words">{{ value }}</code>
<div v-else-if="prop.schema?.kind === 'enum' && prop.schema.type !== 'boolean' && startsWithCapital(prop.schema.type) && !prop.schema.type.startsWith(prop.schema.schema[0])" class="flex items-center flex-wrap gap-1 -my-1">
<code v-for="value in prop.schema.schema.filter(value => typeof value === 'string')" :key="value" class="whitespace-pre-wrap break-words leading-6">{{ value }}</code>
</div>
</Field>
</template>

View File

@@ -10,6 +10,7 @@ const options = [
const state = reactive({
input: undefined,
inputMenu: undefined,
textarea: undefined,
select: undefined,
selectMenu: undefined,
@@ -23,6 +24,9 @@ const state = reactive({
const schema = z.object({
input: z.string().min(10),
inputMenu: z.any().refine(option => option?.value === 'option-2', {
message: 'Select Option 2'
}),
textarea: z.string().min(10),
select: z.string().refine(value => value === 'option-2', {
message: 'Select Option 2'
@@ -61,6 +65,10 @@ async function onSubmit (event: FormSubmitEvent<Schema>) {
<UInput v-model="state.input" />
</UFormGroup>
<UFormGroup name="inputMenu" label="Input Menu">
<UInputMenu v-model="state.inputMenu" :options="options" />
</UFormGroup>
<UFormGroup name="textarea" label="Textarea">
<UTextarea v-model="state.textarea" />
</UFormGroup>

View File

@@ -0,0 +1,9 @@
<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(people[0])
</script>
<template>
<UInputMenu v-model="selected" :options="people" />
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const people = []
const selected = ref()
</script>
<template>
<UInputMenu v-model="selected" :options="people">
<template #empty>
No people
</template>
</UInputMenu>
</template>

View File

@@ -0,0 +1,36 @@
<script setup>
const people = [{
id: 'benjamincanac',
label: 'benjamincanac',
href: 'https://github.com/benjamincanac',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
}, {
id: 'Atinux',
label: 'Atinux',
href: 'https://github.com/Atinux',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
}, {
id: 'smarroufin',
label: 'smarroufin',
href: 'https://github.com/smarroufin',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' }
}, {
id: 'nobody',
label: 'Nobody',
icon: 'i-heroicons-user-circle'
}]
const selected = ref(people[0])
</script>
<template>
<UInputMenu v-model="selected" :options="people">
<template #leading>
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4 mx-0.5" />
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" class="mx-0.5" />
</template>
</UInputMenu>
</template>

View File

@@ -0,0 +1,26 @@
<script setup>
const people = [{
id: 1,
name: 'Wade Cooper'
}, {
id: 2,
name: 'Arlene Mccoy'
}, {
id: 3,
name: 'Devon Webb'
}, {
id: 4,
name: 'Tom Cook'
}]
const selected = ref(people[0].name)
</script>
<template>
<UInputMenu
v-model="selected"
:options="people"
value-attribute="name"
option-attribute="name"
/>
</template>

View File

@@ -0,0 +1,13 @@
<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(people[0])
</script>
<template>
<UInputMenu v-model="selected" :options="people" searchable>
<template #option-empty="{ query }">
<q>{{ query }}</q> not found
</template>
</UInputMenu>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
const people = [
{ name: 'Wade Cooper', online: true },
{ name: 'Arlene Mccoy', online: false },
{ name: 'Devon Webb', online: false },
{ name: 'Tom Cook', online: true },
{ name: 'Tanya Fox', online: false },
{ name: 'Hellen Schmidt', online: true },
{ name: 'Caroline Schultz', online: true },
{ name: 'Mason Heaney', online: false },
{ name: 'Claudie Smitham', online: true },
{ name: 'Emil Schaefer', online: false }
]
const selected = ref(people[3])
</script>
<template>
<UInputMenu v-model="selected" :options="people" option-attribute="name">
<template #option="{ option: person }">
<span :class="[person.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
<span class="truncate">{{ person.name }}</span>
</template>
</UInputMenu>
</template>

View File

@@ -0,0 +1,9 @@
<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(people[0])
</script>
<template>
<UInputMenu v-model="selected" :options="people" :popper="{ arrow: true }" />
</template>

View File

@@ -0,0 +1,9 @@
<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(people[0])
</script>
<template>
<UInputMenu v-model="selected" :options="people" :popper="{ offsetDistance: 0 }" />
</template>

View File

@@ -0,0 +1,9 @@
<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(people[0])
</script>
<template>
<UInputMenu v-model="selected" :options="people" :popper="{ placement: 'right-start' }" />
</template>

View File

@@ -0,0 +1,28 @@
<script setup>
const options = [
{ id: 1, name: 'Wade Cooper', colors: ['red', 'yellow'] },
{ id: 2, name: 'Arlene Mccoy', colors: ['blue', 'yellow'] },
{ id: 3, name: 'Devon Webb', colors: ['green', 'blue'] },
{ id: 4, name: 'Tom Cook', colors: ['blue', 'red'] },
{ id: 5, name: 'Tanya Fox', colors: ['green', 'red'] },
{ id: 5, name: 'Hellen Schmidt', colors: ['green', 'yellow'] }
]
const selected = ref(options[1])
</script>
<template>
<UInputMenu
v-model="selected"
:options="options"
placeholder="Select a person"
by="id"
option-attribute="name"
:search-attributes="['name', 'colors']"
>
<template #option="{ option: person }">
<span v-for="color in person.colors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
<span class="truncate">{{ person.name }}</span>
</template>
</UInputMenu>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const people = []
const selected = ref()
</script>
<template>
<USelectMenu v-model="selected" :options="people">
<template #empty>
No people
</template>
</USelectMenu>
</template>

View File

@@ -28,11 +28,9 @@ const selected = ref(people[0])
<template>
<USelectMenu v-model="selected" :options="people">
<template #label>
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4" />
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" />
{{ selected.label }}
<template #leading>
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4 mx-0.5" />
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" class="mx-0.5" />
</template>
</USelectMenu>
</template>

View File

@@ -13,9 +13,7 @@ const people = [{
name: 'Tom Cook'
}]
const selected = ref(people[0].id)
const current = computed(() => people.find(person => person.id === selected.value))
const selected = ref(people[0].name)
</script>
<template>
@@ -23,11 +21,7 @@ const current = computed(() => people.find(person => person.id === selected.valu
v-model="selected"
:options="people"
placeholder="Select people"
value-attribute="id"
value-attribute="name"
option-attribute="name"
>
<template #label>
{{ current.name }}
</template>
</USelectMenu>
/>
</template>

View File

@@ -1,11 +1,11 @@
<script setup>
const options = [
{ id: 1, name: 'Wade Cooper', favoriteColors: ['red', 'yellow'] },
{ id: 2, name: 'Arlene Mccoy', favoriteColors: ['blue', 'yellow'] },
{ id: 3, name: 'Devon Webb', favoriteColors: ['green', 'blue'] },
{ id: 4, name: 'Tom Cook', favoriteColors: ['blue', 'red'] },
{ id: 5, name: 'Tanya Fox', favoriteColors: ['green', 'red'] },
{ id: 5, name: 'Hellen Schmidt', favoriteColors: ['green', 'yellow'] }
{ id: 1, name: 'Wade Cooper', colors: ['red', 'yellow'] },
{ id: 2, name: 'Arlene Mccoy', colors: ['blue', 'yellow'] },
{ id: 3, name: 'Devon Webb', colors: ['green', 'blue'] },
{ id: 4, name: 'Tom Cook', colors: ['blue', 'red'] },
{ id: 5, name: 'Tanya Fox', colors: ['green', 'red'] },
{ id: 5, name: 'Hellen Schmidt', colors: ['green', 'yellow'] }
]
const selected = ref(options[1])
@@ -15,16 +15,15 @@ const selected = ref(options[1])
<USelectMenu
v-model="selected"
:options="options"
class="w-full lg:w-96"
placeholder="Select an user"
placeholder="Select a person"
searchable
searchable-placeholder="Search by name or favorite colors"
searchable-placeholder="Search by name or color"
option-attribute="name"
by="id"
:search-attributes="['name', 'favoriteColors']"
:search-attributes="['name', 'colors']"
>
<template #option="{ option: person }">
<span v-for="color in person.favoriteColors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
<span v-for="color in person.colors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
<span class="truncate">{{ person.name }}</span>
</template>
</USelectMenu>

View File

@@ -6,7 +6,7 @@
<NuxtLink v-if="date.release" :to="`https://github.com/nuxt/ui/releases/tag/${date.release.name}`" target="_blank" class="text-gray-900 dark:text-white font-bold text-3xl mt-2 group hover:text-primary-500 dark:hover:text-primary-400 transition-[color]">
{{ date.release.name }}
</NuxtLink>
<ul v-else-if="date.pulls?.length" class="mt-2 space-y-1 text-gray-600 dark:text-gray-300">
<ul v-if="date.pulls?.length" class="mt-2 space-y-1 text-gray-600 dark:text-gray-300">
<li v-for="pull in date.pulls" :key="pull.id" class="text-sm/6 break-all">
<NuxtLink :to="`https://github.com/${pull.user.login}`" target="_blank" class="text-gray-900 dark:text-white transition-colors inline-flex items-center gap-1 rounded-full bg-gray-100/50 dark:bg-gray-800/50 dark:hover:bg-gray-800 p-0.5 pr-1 ring-1 ring-gray-300 dark:ring-gray-700 text-xs font-medium flex-shrink-0 align-middle">
<UAvatar :src="`https://github.com/${pull.user.login}.png`" size="3xs" />

View File

@@ -14,8 +14,9 @@ links:
Pass an array of arrays to the `items` prop of the Dropdown component. Each array represents a group of items. Each item can have the following properties:
- `label` - The label of the item.
- `labelClass` - The class of the item label. :u-badge{label="New" class="!rounded-full" variant="subtle"}
- `icon` - The icon of the item.
- `iconClass` - The class of the icon of the item.
- `iconClass` - The class of the item icon.
- `avatar` - The avatar of the item. You can pass all the props of the [Avatar](/elements/avatar) component.
- `shortcuts` - The shortcuts of the item.
- `slot` - The slot of the item.

View File

@@ -172,13 +172,13 @@ Use the `#leading` slot to set the content of the leading icon.
::component-card
---
slots:
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5" />
baseProps:
placeholder: 'Search...'
---
#leading
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5"}
::
### `trailing`

View File

@@ -0,0 +1,170 @@
---
title: InputMenu
description: Display an autocomplete input with real-time suggestions.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/InputMenu.vue
- label: 'Combobox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/combobox'
navigation:
badge: New
---
## Usage
The `InputMenu` component renders by default an [Input](/forms/input) component and is based on the `ui.input` preset. You can use most of the `Input` props to configure the display such as [color](/forms/input#style), [variant](/forms/input#style), [size](/forms/input#size), [placeholder](/forms/input#placeholder), [icon](/forms/input#icon), [disabled](/forms/input#disabled), etc.
You can use the `ui` prop like the `Input` component to override the default config. The `uiMenu` prop can be used to override the default menu config.
Pass an array of strings or objects to the `options` prop to display in the menu.
::component-example
---
component: 'input-menu-example-basic'
componentProps:
class: 'w-full lg:w-48'
---
::
::callout{icon="i-heroicons-exclamation-triangle"}
This component does not support multiple values. Use the [SelectMenu](/forms/select-menu#multiple) component instead.
::
### Objects
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`.
::component-example
---
component: 'input-menu-example-objects'
componentProps:
class: 'w-full lg:w-48'
---
::
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'
---
::
If you only want to select a single object property rather than the whole object as value, you can set the `value-attribute` property. This prop defaults to `null`.
::component-example
---
component: 'input-menu-example-objects-value-attribute'
componentProps:
class: 'w-full lg:w-48'
---
::
### Icon
The `InputMenu` has a button on the right to toggle the menu. Use the `trailing-icon` prop to set a different icon or change it globally in `ui.inputMenu.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
::component-card
---
baseProps:
class: 'w-full lg:w-48'
placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
trailingIcon: 'i-heroicons-chevron-up-down-20-solid'
excludedProps:
- trailingIcon
---
::
Use the `selected-icon` prop to set a different icon or change it globally in `ui.inputMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
::component-card
---
baseProps:
class: 'w-full lg:w-48'
placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
selectedIcon: 'i-heroicons-hand-thumb-up-solid'
excludedProps:
- selectedIcon
---
::
::callout{icon="i-heroicons-light-bulb"}
Learn how to customize icons from the [Input](/forms/input#icon) component.
::
## Popper
Use the `popper` prop to customize the popper instance.
### Arrow
:component-example{component="input-menu-example-popper-arrow"}
### Placement
:component-example{component="input-menu-example-popper-placement"}
### Offset
:component-example{component="input-menu-example-popper-offset"}
## Slots
### `option`
Use the `#option` slot to customize the option content. You will have access to the `option`, `active` and `selected` properties in the slot scope.
::component-example
---
component: 'input-menu-example-option-slot'
componentProps:
class: 'w-full lg:w-48'
---
::
### `option-empty`
Use the `#option-empty` slot to customize the content displayed when the `searchable` prop is `true` and there is no options. You will have access to the `query` property in the slot scope.
::component-example
---
component: 'input-menu-example-option-empty-slot'
componentProps:
class: 'w-full lg:w-48'
---
::
### `empty`
Use the `#empty` slot to customize the content displayed when there is no options. Defaults to `No options.`.
::component-example
---
component: 'input-menu-example-empty-slot'
componentProps:
class: 'w-full lg:w-48'
---
::
## Props
:component-props
## Config
::callout{icon="i-heroicons-light-bulb"}
Use the `ui` prop to override the input config and the `uiMenu` prop to override the menu config.
::
::tabs{:selectedIndex="1"}
:component-preset{label="Input (ui)" slug="Input"}
:component-preset{label="InputMenu (uiMenu)"}
::

View File

@@ -203,7 +203,7 @@ Use the `#leading` slot to set the content of the leading icon.
::component-card
---
slots:
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5" />
baseProps:
options:
- 'United States'
@@ -213,7 +213,7 @@ baseProps:
---
#leading
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5"}
::
### `trailing`

View File

@@ -22,7 +22,7 @@ Like the `Select` component, you can use the `options` prop to pass an array of
---
component: 'select-menu-example-basic'
componentProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
---
::
@@ -34,7 +34,7 @@ You can use the `multiple` prop to select multiple values.
---
component: 'select-menu-example-multiple'
componentProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
---
::
@@ -46,7 +46,7 @@ You can pass an array of objects to `options` and either compare on the whole ob
---
component: 'select-menu-example-objects'
componentProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
---
::
@@ -56,7 +56,7 @@ If you only want to select a single object property rather than the whole object
---
component: 'select-menu-example-objects-value-attribute'
componentProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
---
::
@@ -67,7 +67,7 @@ Use the `selected-icon` prop to set a different icon or change it globally in `u
::component-card
---
baseProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
@@ -81,7 +81,7 @@ excludedProps:
Learn how to customize icons from the [Select](/forms/select#icon) component.
::
### Search
## Searchable
Use the `searchable` prop to enable search.
@@ -92,7 +92,7 @@ This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) compon
::component-card
---
baseProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
@@ -101,28 +101,26 @@ props:
---
::
#### Search Attributes
### Attributes
Use the `search-attributes` 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.
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: 'select-menu-example-search-attributes'
componentProps:
class: 'w-full lg:w-96'
class: 'w-full lg:w-48'
---
::
#### Clear on close
### Clear on close
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.
::component-card
---
baseProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
placeholder: 'Select a person'
searchable: true
searchablePlaceholder: 'Search a person...'
@@ -142,11 +140,11 @@ Use the `debounce` prop to adjust the delay of the function.
---
component: 'select-menu-example-async-search'
componentProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
---
::
### Create option
## Creatable
Use the `creatable` prop to enable the creation of new options when the search doesn't return any results (only works with `searchable`).
@@ -156,7 +154,7 @@ Try to search for something that doesn't exist in the example below.
---
component: 'select-menu-example-creatable'
componentProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
---
::
@@ -186,7 +184,7 @@ You can override the `#label` slot and handle the display yourself.
---
component: 'select-menu-example-multiple-slot'
componentProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
---
::
@@ -198,7 +196,7 @@ You can also override the `#default` slot entirely.
---
component: 'select-menu-example-button'
componentProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
---
::
@@ -210,7 +208,7 @@ Use the `#option` slot to customize the option content. You will have access to
---
component: 'select-menu-example-option-slot'
componentProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
---
::
@@ -222,7 +220,7 @@ Use the `#option-empty` slot to customize the content displayed when the `search
---
component: 'select-menu-example-option-empty-slot'
componentProps:
class: 'w-full lg:w-40'
class: 'w-full lg:w-48'
---
::
@@ -234,6 +232,18 @@ Use the `#option-create` slot to customize the content displayed when the `creat
An example is available in the [Create option](#create-option) section.
::
### `empty` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Use the `#empty` slot to customize the content displayed when there is no options. Defaults to `No options.`.
::component-example
---
component: 'select-menu-example-empty-slot'
componentProps:
class: 'w-full lg:w-48'
---
::
## Props
:component-props
@@ -248,5 +258,3 @@ Use the `ui` prop to override the select config and the `uiMenu` prop to overrid
:component-preset{label="Select (ui)" slug="Select"}
:component-preset{label="SelectMenu (uiMenu)"}
::
:component-preset

View File

@@ -12,8 +12,9 @@ links:
Pass an array to the `links` prop of the VerticalNavigation component. Each link can have the following properties:
- `label` - The label of the link.
- `labelClass` - The class of the link label. :u-badge{label="New" class="!rounded-full" variant="subtle"}
- `icon` - The icon of the link.
- `iconClass` - The class of the icon link.
- `iconClass` - The class of the link icon.
- `avatar` - The avatar of the link. You can pass all the props of the [Avatar](/elements/avatar) component.
- `badge` - A badge to display next to the label.
- `click` - The click handler of the link.

View File

@@ -8,8 +8,9 @@ description: A list of links that indicate the current page's location within a
Pass an array to the `links` prop of the Breadcrumb component. Each link can have the following properties:
- `label` - The label of the link.
- `labelClass` - The class of the link label. :u-badge{label="New" class="!rounded-full" variant="subtle"}
- `icon` - The icon of the link.
- `iconClass` - The class of the icon link.
- `iconClass` - The class of the link icon.
You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.

View File

@@ -67,8 +67,8 @@ sections:
color: white
size: lg
trailingIcon: i-heroicons-arrow-right-20-solid
- title: 'A collection of <span class="text-primary">30+</span> components'
description: 'Get access to 30+ beautifully designed and fully customizable components built for Nuxt. These components<br class="hidden lg:block"> are updated regularly to ensure that you always have the latest features and functionalities.'
- title: 'A collection of <span class="text-primary">40+</span> components'
description: 'Get access to 40+ beautifully designed and fully customizable components built for Nuxt. These components<br class="hidden lg:block"> are updated regularly to ensure that you always have the latest features and functionalities.'
class: 'dark:bg-gradient-to-b from-gray-950/50 to-gray-900'
slot: categories
links:
@@ -82,12 +82,12 @@ sections:
to: /elements/dropdown
image:
path: /illustrations/elements
badge: 9
badge: 15
- label: Forms
to: /forms/form
image:
path: /illustrations/forms
badge: 10
badge: 12
- label: Data
to: /data/table
image:
@@ -97,17 +97,17 @@ sections:
to: /navigation/command-palette
image:
path: /illustrations/navigation
badge: 4
badge: 5
- label: Overlays
to: /overlays/modal
image:
path: /illustrations/overlays
badge: 6
badge: 7
- label: Layout
to: /layout/card
image:
path: /illustrations/layout
badge: 3
badge: 4
cta:
title: Trusted and supported by our<br class="hidden lg:block"> amazing community
pro:

View File

@@ -10,7 +10,7 @@
"@nuxt/content": "^2.9.0",
"@nuxt/devtools": "^1.0.4",
"@nuxt/eslint-config": "^0.2.0",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.5.0-28367445.03c79ba",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.6.0-28371586.2e2b852",
"@nuxthq/studio": "^1.0.5",
"@nuxtjs/fontaine": "^0.4.1",
"@nuxtjs/google-fonts": "^3.1.0",

View File

@@ -17,6 +17,13 @@
<UDivider v-if="page.body?.toc?.links?.length" type="dashed" />
<UPageLinks title="Community" :links="links" />
<UDivider type="dashed" />
<div class="space-y-3">
<AdsPro />
<AdsCarbon />
</div>
</div>
</template>
</UDocsToc>

View File

@@ -53,7 +53,7 @@
"ohash": "^1.1.3",
"pathe": "^1.1.1",
"scule": "^1.1.1",
"tailwind-merge": "^1.14.0",
"tailwind-merge": "^2.1.0",
"tailwindcss": "^3.3.6"
},
"devDependencies": {

6
playground/app.config.ts Normal file
View File

@@ -0,0 +1,6 @@
export default defineAppConfig({
ui: {
primary: 'green',
gray: 'cool'
}
})

View File

@@ -0,0 +1,6 @@
import type { Config } from 'tailwindcss'
export default <Partial<Config>>{
theme: {
}
}

87
pnpm-lock.yaml generated
View File

@@ -77,8 +77,8 @@ importers:
specifier: ^1.1.1
version: 1.1.1
tailwind-merge:
specifier: ^1.14.0
version: 1.14.0
specifier: ^2.1.0
version: 2.1.0
tailwindcss:
specifier: ^3.3.6
version: 3.3.6
@@ -154,8 +154,8 @@ importers:
specifier: ^0.2.0
version: 0.2.0(eslint@8.55.0)
'@nuxt/ui-pro':
specifier: npm:@nuxt/ui-pro-edge@0.5.0-28367445.03c79ba
version: /@nuxt/ui-pro-edge@0.5.0-28367445.03c79ba(nuxt@3.8.2)(rollup@3.29.4)(vite@5.0.5)(vue@3.3.8)
specifier: npm:@nuxt/ui-pro-edge@0.6.0-28371586.2e2b852
version: /@nuxt/ui-pro-edge@0.6.0-28371586.2e2b852(nuxt@3.8.2)(rollup@3.29.4)(vite@5.0.5)(vue@3.3.8)
'@nuxthq/studio':
specifier: ^1.0.5
version: 1.0.5(rollup@3.29.4)
@@ -539,7 +539,6 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.0
dev: true
/@babel/standalone@7.23.5:
resolution: {integrity: sha512-4bqgawmyDPu+9gQhZOKh1ftCUa6BAT0KztElMcWAJgOgQJRNhmGVA0M0McedEqvGi7SbfiBBvlH13Jc47P919A==}
@@ -1646,8 +1645,41 @@ packages:
- rollup
- supports-color
/@nuxt/ui-edge@2.11.0-28367442.db508b2(nuxt@3.8.2)(rollup@3.29.4)(vite@5.0.5)(vue@3.3.8):
resolution: {integrity: sha512-8ldVGZwowLtecWko9DT+LaepLIEHrUvvIu18bzd+mwzzOBo0Q4DZFQkdHpHVMneFxQNgSoc6tlq1TMZ43ZTz9A==}
/@nuxt/ui-pro-edge@0.6.0-28371586.2e2b852(nuxt@3.8.2)(rollup@3.29.4)(vite@5.0.5)(vue@3.3.8):
resolution: {integrity: sha512-1MER3QAUBCmqEAKEQAJBXVfw5JFll5uMiNvZO9w5q7DeiBbb9kbugwASlpo+20RrwZJw/mRPH3oQeF5dUXGajQ==}
dependencies:
'@nuxt/ui': 2.11.1(nuxt@3.8.2)(rollup@3.29.4)(vite@5.0.5)(vue@3.3.8)
'@vueuse/core': 10.7.0(vue@3.3.8)
defu: 6.1.3
ofetch: 1.3.3
pathe: 1.1.1
tailwind-merge: 1.14.0
transitivePeerDependencies:
- '@vue/composition-api'
- async-validator
- axios
- change-case
- drauu
- focus-trap
- idb-keyval
- jwt-decode
- nprogress
- nuxt
- qrcode
- rollup
- sortablejs
- supports-color
- ts-node
- universal-cookie
- vite
- vue
dev: true
/@nuxt/ui-templates@1.3.1:
resolution: {integrity: sha512-5gc02Pu1HycOVUWJ8aYsWeeXcSTPe8iX8+KIrhyEtEoOSkY0eMBuo0ssljB8wALuEmepv31DlYe5gpiRwkjESA==}
/@nuxt/ui@2.11.1(nuxt@3.8.2)(rollup@3.29.4)(vite@5.0.5)(vue@3.3.8):
resolution: {integrity: sha512-g+I2qnW1oYeo5xGhA0DsNu8zT5rTbCqt/APocDvYwdNdssrs1ra7mBERcEVkyDIyg2UR6EzTgV2mngLSLBID7g==}
engines: {node: '>=v16.20.2'}
dependencies:
'@egoist/tailwindcss-icons': 1.4.0(tailwindcss@3.3.6)
@@ -1694,39 +1726,6 @@ packages:
- vue
dev: true
/@nuxt/ui-pro-edge@0.5.0-28367445.03c79ba(nuxt@3.8.2)(rollup@3.29.4)(vite@5.0.5)(vue@3.3.8):
resolution: {integrity: sha512-u/3YZu8n/aji+noPlyyrM0H3yecPfOnilbULZ6T9h+u6/Pd1t8xP7oWRvTbaPTSc5z+GKocd2sFiv1gHPSm3OA==}
dependencies:
'@nuxt/ui': /@nuxt/ui-edge@2.11.0-28367442.db508b2(nuxt@3.8.2)(rollup@3.29.4)(vite@5.0.5)(vue@3.3.8)
'@vueuse/core': 10.7.0(vue@3.3.8)
defu: 6.1.3
ofetch: 1.3.3
pathe: 1.1.1
tailwind-merge: 1.14.0
transitivePeerDependencies:
- '@vue/composition-api'
- async-validator
- axios
- change-case
- drauu
- focus-trap
- idb-keyval
- jwt-decode
- nprogress
- nuxt
- qrcode
- rollup
- sortablejs
- supports-color
- ts-node
- universal-cookie
- vite
- vue
dev: true
/@nuxt/ui-templates@1.3.1:
resolution: {integrity: sha512-5gc02Pu1HycOVUWJ8aYsWeeXcSTPe8iX8+KIrhyEtEoOSkY0eMBuo0ssljB8wALuEmepv31DlYe5gpiRwkjESA==}
/@nuxt/vite-builder@3.8.2(eslint@8.55.0)(rollup@3.29.4)(typescript@5.3.3)(vue-tsc@1.8.25)(vue@3.3.8):
resolution: {integrity: sha512-l/lzDDTbd3M89BpmWqjhVLgLVRqfkKp0tyYgV5seJQjj3SX+IeqI7k6k8+dMEifdeO34jUajVWptNpITXQryyg==}
engines: {node: ^14.18.0 || >=16.10.0}
@@ -10411,7 +10410,6 @@ packages:
/regenerator-runtime@0.14.0:
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
dev: true
/regexp.prototype.flags@1.5.1:
resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==}
@@ -11387,6 +11385,13 @@ packages:
/tailwind-merge@1.14.0:
resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==}
dev: true
/tailwind-merge@2.1.0:
resolution: {integrity: sha512-l11VvI4nSwW7MtLSLYT4ldidDEUwQAMWuSHk7l4zcXZDgnCRa0V3OdCwFfM7DCzakVXMNRwAeje9maFFXT71dQ==}
dependencies:
'@babel/runtime': 7.23.5
dev: false
/tailwindcss@3.3.6:
resolution: {integrity: sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==}

View File

@@ -75,7 +75,7 @@ import UButton from '../elements/Button.vue'
import UIcon from '../elements/Icon.vue'
import UCheckbox from '../forms/Checkbox.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig, omit, get } from '../../utils'
import { mergeConfig, get } from '../../utils'
import type { Strategy, Button } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
@@ -104,7 +104,7 @@ export default defineComponent({
default: () => defaultComparator
},
rows: {
type: Array as PropType<{ [key: string]: any, click?: Function }[]>,
type: Array as PropType<{ [key: string]: any }[]>,
default: () => []
},
columns: {
@@ -156,7 +156,7 @@ export default defineComponent({
setup (props, { emit, attrs: $attrs }) {
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class'))
const columns = computed(() => props.columns ?? Object.keys(omit(props.rows[0] ?? {}, ['click'])).map((key) => ({ key, label: upperFirst(key), sortable: false })))
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: upperFirst(key), sortable: false })))
const sort = ref(defu({}, props.sort, { column: null, direction: 'asc' }))

View File

@@ -34,13 +34,13 @@
<script lang="ts">
import { computed, toRef, defineComponent } 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 UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue'
import { useUI } from '../../composables/useUI'
import type { Avatar, Button, AlertColor, AlertVariant, Strategy } from '../../types'
import { mergeConfig } from '../../utils'
import { mergeConfig, twMerge } from '../../utils'
// @ts-expect-error
import appConfig from '#build/app.config'
import { alert } from '#ui/ui.config'

View File

@@ -22,10 +22,10 @@
<script lang="ts">
import { defineComponent, ref, computed, toRef, watch } 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 { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import { mergeConfig, twMerge } from '../../utils'
import type { AvatarSize, AvatarChipColor, AvatarChipPosition, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'

View File

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

View File

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

View File

@@ -19,11 +19,11 @@
<script lang="ts">
import { computed, defineComponent, toRef } 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 ULink from '../elements/Link.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import { mergeConfig, twMerge } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { ButtonColor, ButtonSize, ButtonVariant, Strategy } from '../../types'
// @ts-expect-error

View File

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

View File

@@ -21,7 +21,7 @@
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(ui.arrow)" />
<HMenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.height]" static>
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
<NuxtLink v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ href, target, rel, navigate, isExternal }" v-bind="omit(item, ['label', 'slot', 'icon', 'iconClass', 'avatar', 'shortcuts', 'disabled', 'click'])" custom>
<NuxtLink v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ href, target, rel, navigate, isExternal }" v-bind="omit(item, ['label', 'labelClass', 'slot', 'icon', 'iconClass', 'avatar', 'shortcuts', 'disabled', 'click'])" custom>
<HMenuItem v-slot="{ active, disabled: itemDisabled, close }" :disabled="item.disabled">
<component
:is="!!href ? 'a' : 'button'"
@@ -32,10 +32,10 @@
@click="onClick($event, item, { href, navigate, close, isExternal })"
>
<slot :name="item.slot || 'item'" :item="item">
<UIcon v-if="item.icon" :name="item.icon" :class="[ui.item.icon.base, active ? ui.item.icon.active : ui.item.icon.inactive, item.iconClass]" />
<UIcon v-if="item.icon" :name="item.icon" :class="twMerge(twJoin(ui.item.icon.base, active ? ui.item.icon.active : ui.item.icon.inactive), item.iconClass)" />
<UAvatar v-else-if="item.avatar" v-bind="{ size: ui.item.avatar.size, ...item.avatar }" :class="ui.item.avatar.base" />
<span :class="ui.item.label">{{ item.label }}</span>
<span :class="twMerge(ui.item.label, item.labelClass)">{{ item.label }}</span>
<span v-if="item.shortcuts?.length" :class="ui.item.shortcuts">
<UKbd v-for="shortcut of item.shortcuts" :key="shortcut">{{ shortcut }}</UKbd>
@@ -57,12 +57,13 @@ import { defineComponent, ref, computed, toRef, onMounted, resolveComponent } fr
import type { PropType } from 'vue'
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
import { defu } from 'defu'
import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { mergeConfig, omit } from '../../utils'
import { mergeConfig, twMerge, omit } from '../../utils'
import type { DropdownItem, PopperOptions, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
@@ -214,6 +215,8 @@ export default defineComponent({
onMouseLeave,
onClick,
omit,
twMerge,
twJoin,
NuxtLink
}
}

View File

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

View File

@@ -21,7 +21,7 @@
:role="disabled ? 'link' : undefined"
:rel="rel"
:target="target"
:class="active ? activeClass : resolveLinkClass(route, $route, { isActive, isExactActive })"
:class="active !== undefined ? (active ? activeClass : inactiveClass) : resolveLinkClass(route, $route, { isActive, isExactActive })"
@click="(e) => !isExternal && navigate(e)"
>
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
@@ -48,7 +48,7 @@ export default defineComponent({
},
active: {
type: Boolean,
default: false
default: undefined
},
exact: {
type: Boolean,

View File

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

View File

@@ -34,12 +34,12 @@
<script lang="ts">
import { ref, computed, toRef, onMounted, defineComponent } 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 { defu } from 'defu'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig, looseToNumber } from '../../utils'
import { mergeConfig, twMerge, looseToNumber } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { InputSize, InputColor, InputVariant, Strategy } from '../../types'
// @ts-expect-error

View File

@@ -0,0 +1,411 @@
<template>
<HCombobox
v-slot="{ open }"
:by="by"
:name="name"
:model-value="modelValue"
:disabled="disabled || loading"
as="div"
:class="ui.wrapper"
@update:model-value="onUpdate"
>
<div :class="uiMenu.trigger">
<HComboboxInput
:id="inputId"
ref="input"
:name="name"
:required="required"
:placeholder="placeholder"
:disabled="disabled || loading"
:class="inputClass"
autocomplete="off"
v-bind="attrs"
:display-value="() => ['string', 'number'].includes(typeof modelValue) ? modelValue : modelValue[optionAttribute]"
@change="query = $event.target.value"
/>
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<HComboboxButton v-if="(isTrailing && trailingIconName) || $slots.trailing" ref="trigger" :class="trailingWrapperIconClass">
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</HComboboxButton>
</div>
<div v-if="open" ref="container" :class="[uiMenu.container, uiMenu.width]">
<Transition appear v-bind="uiMenu.transition">
<div>
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(uiMenu.arrow)" />
<HComboboxOptions static :class="[uiMenu.base, uiMenu.ring, uiMenu.rounded, uiMenu.shadow, uiMenu.background, uiMenu.padding, uiMenu.height]">
<HComboboxOption
v-for="(option, index) in filteredOptions"
v-slot="{ active, selected, disabled: optionDisabled }"
:key="index"
as="template"
:value="valueAttribute ? option[valueAttribute] : option"
:disabled="option.disabled"
>
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive, selected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
<div :class="uiMenu.option.container">
<slot name="option" :option="option" :active="active" :selected="selected">
<UIcon v-if="option.icon" :name="option.icon" :class="[uiMenu.option.icon.base, active ? uiMenu.option.icon.active : uiMenu.option.icon.inactive, option.iconClass]" aria-hidden="true" />
<UAvatar
v-else-if="option.avatar"
v-bind="{ size: uiMenu.option.avatar.size, ...option.avatar }"
:class="uiMenu.option.avatar.base"
aria-hidden="true"
/>
<span v-else-if="option.chip" :class="uiMenu.option.chip.base" :style="{ background: `#${option.chip}` }" />
<span class="truncate">{{ ['string', 'number'].includes(typeof option) ? option : option[optionAttribute] }}</span>
</slot>
</div>
<span v-if="selected" :class="[uiMenu.option.selectedIcon.wrapper, uiMenu.option.selectedIcon.padding]">
<UIcon :name="selectedIcon" :class="uiMenu.option.selectedIcon.base" aria-hidden="true" />
</span>
</li>
</HComboboxOption>
<p v-if="query && !filteredOptions.length" :class="uiMenu.option.empty">
<slot name="option-empty" :query="query">
No results for "{{ query }}".
</slot>
</p>
<p v-else-if="!filteredOptions.length" :class="uiMenu.empty">
<slot name="empty" :query="query">
No options.
</slot>
</p>
</HComboboxOptions>
</div>
</Transition>
</div>
</HCombobox>
</template>
<script lang="ts">
import { ref, computed, toRef, watch, defineComponent } from 'vue'
import type { PropType } from 'vue'
import {
Combobox as HCombobox,
ComboboxButton as HComboboxButton,
ComboboxOptions as HComboboxOptions,
ComboboxOption as HComboboxOption,
ComboboxInput as HComboboxInput
} from '@headlessui/vue'
import { computedAsync } from '@vueuse/core'
import { defu } from 'defu'
import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { useFormGroup } from '../../composables/useFormGroup'
import { get, mergeConfig, twMerge } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { InputSize, InputColor, InputVariant, PopperOptions, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { input, inputMenu } from '#ui/ui.config'
const config = mergeConfig<typeof input>(appConfig.ui.strategy, appConfig.ui.input, input)
const configMenu = mergeConfig<typeof inputMenu>(appConfig.ui.strategy, appConfig.ui.inputMenu, inputMenu)
export default defineComponent({
components: {
HCombobox,
HComboboxButton,
HComboboxOptions,
HComboboxOption,
HComboboxInput,
UIcon,
UAvatar
},
inheritAttrs: false,
props: {
modelValue: {
type: [String, Number, Object, Array],
default: ''
},
by: {
type: String,
default: undefined
},
options: {
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
default: () => []
},
id: {
type: String,
default: null
},
name: {
type: String,
default: null
},
required: {
type: Boolean,
default: false
},
icon: {
type: String,
default: null
},
loadingIcon: {
type: String,
default: () => config.default.loadingIcon
},
leadingIcon: {
type: String,
default: null
},
trailingIcon: {
type: String,
default: () => configMenu.default.trailingIcon
},
trailing: {
type: Boolean,
default: false
},
leading: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
selectedIcon: {
type: String,
default: () => configMenu.default.selectedIcon
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: null
},
padded: {
type: Boolean,
default: true
},
size: {
type: String as PropType<InputSize>,
default: null,
validator (value: string) {
return Object.keys(config.size).includes(value)
}
},
color: {
type: String as PropType<InputColor>,
default: () => config.default.color,
validator (value: string) {
return [...appConfig.ui.colors, ...Object.keys(config.color)].includes(value)
}
},
variant: {
type: String as PropType<InputVariant>,
default: () => config.default.variant,
validator (value: string) {
return [
...Object.keys(config.variant),
...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
optionAttribute: {
type: String,
default: 'label'
},
valueAttribute: {
type: String,
default: null
},
searchAttributes: {
type: Array,
default: null
},
popper: {
type: Object as PropType<PopperOptions>,
default: () => ({})
},
inputClass: {
type: String,
default: null
},
class: {
type: [String, Object, Array] as PropType<any>,
default: () => ''
},
ui: {
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
default: () => ({})
},
uiMenu: {
type: Object as PropType<Partial<typeof configMenu> & { strategy?: Strategy }>,
default: () => ({})
}
},
emits: ['update:modelValue', 'open', 'close', 'change'],
setup (props, { emit, slots }) {
const { ui, attrs } = useUI('input', toRef(props, 'ui'), config, toRef(props, 'class'))
const { ui: uiMenu } = useUI('inputMenu', toRef(props, 'uiMenu'), configMenu)
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
const [trigger, container] = usePopper(popper.value)
const { size: sizeButtonGroup, rounded } = useInjectButtonGroup({ ui, props })
const { emitFormBlur, emitFormChange, inputId, color, size: sizeFormGroup, name } = useFormGroup(props, config)
const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value)
const query = ref('')
const inputClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
return twMerge(twJoin(
ui.value.base,
ui.value.form,
rounded.value,
ui.value.placeholder,
ui.value.size[size.value],
props.padded ? ui.value.padding[size.value] : 'p-0',
variant?.replaceAll('{color}', color.value),
(isLeading.value || slots.leading) && ui.value.leading.padding[size.value],
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[size.value]
), props.inputClass)
})
const isLeading = computed(() => {
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
})
const isTrailing = computed(() => {
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
})
const leadingIconName = computed(() => {
if (props.loading) {
return props.loadingIcon
}
return props.leadingIcon || props.icon
})
const trailingIconName = computed(() => {
if (props.loading && !isLeading.value) {
return props.loadingIcon
}
return props.trailingIcon || props.icon
})
const leadingWrapperIconClass = computed(() => {
return twJoin(
ui.value.icon.leading.wrapper,
ui.value.icon.leading.pointer,
ui.value.icon.leading.padding[size.value]
)
})
const leadingIconClass = computed(() => {
return twJoin(
ui.value.icon.base,
color.value && appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
ui.value.icon.size[size.value],
props.loading && ui.value.icon.loading
)
})
const trailingWrapperIconClass = computed(() => {
return twJoin(
ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.padding[size.value]
)
})
const trailingIconClass = computed(() => {
return twJoin(
ui.value.icon.base,
color.value && appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
ui.value.icon.size[size.value],
props.loading && !isLeading.value && ui.value.icon.loading
)
})
const filteredOptions = computedAsync(async () => {
if (query.value === '') {
return props.options
}
return (props.options as any[]).filter((option: any) => {
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => {
if (['string', 'number'].includes(typeof option)) {
return String(option).search(new RegExp(query.value, 'i')) !== -1
}
const child = get(option, searchAttribute)
return child !== null && child !== undefined && String(child).search(new RegExp(query.value, 'i')) !== -1
})
})
})
watch(container, (value) => {
if (value) {
emit('open')
} else {
emit('close')
emitFormBlur()
}
})
function onUpdate (event: any) {
emit('update:modelValue', event)
emit('change', event)
emitFormChange()
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
// eslint-disable-next-line vue/no-dupe-keys
uiMenu,
attrs,
// eslint-disable-next-line vue/no-dupe-keys
name,
inputId,
// eslint-disable-next-line vue/no-dupe-keys
popper,
trigger,
container,
isLeading,
isTrailing,
// eslint-disable-next-line vue/no-dupe-keys
inputClass,
leadingIconName,
leadingIconClass,
leadingWrapperIconClass,
trailingIconName,
trailingIconClass,
trailingWrapperIconClass,
filteredOptions,
query,
onUpdate
}
}
})
</script>

View File

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

View File

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

View File

@@ -55,11 +55,11 @@
<script lang="ts">
import { computed, toRef, defineComponent } 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 { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig, get } from '../../utils'
import { mergeConfig, twMerge, get } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { SelectSize, SelectColor, SelectVariant, Strategy } from '../../types'
// @ts-expect-error

View File

@@ -112,6 +112,11 @@
No results for "{{ query }}".
</slot>
</p>
<p v-else-if="!filteredOptions.length" :class="uiMenu.empty">
<slot name="empty" :query="query">
No options.
</slot>
</p>
</component>
</div>
</Transition>
@@ -135,13 +140,13 @@ import {
} from '@headlessui/vue'
import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge'
import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { useFormGroup } from '../../composables/useFormGroup'
import { get, mergeConfig } from '../../utils'
import { get, mergeConfig, twMerge } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { SelectSize, SelectColor, SelectVariant, PopperOptions, Strategy } from '../../types'
// @ts-expect-error

View File

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

View File

@@ -22,11 +22,11 @@
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from '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 { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig } from '../../utils'
import { mergeConfig, twMerge } from '../../utils'
import type { ToggleSize, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'

View File

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

View File

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

View File

@@ -21,11 +21,11 @@
<script lang="ts">
import { toRef, computed, defineComponent } 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 UAvatar from '../elements/Avatar.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import { mergeConfig, twMerge } from '../../utils'
import type { Avatar, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
v-for="(link, index) of links"
v-slot="{ isActive }"
:key="index"
v-bind="omit(link, ['label', 'icon', 'iconClass', 'avatar', 'badge', 'click'])"
v-bind="omit(link, ['label', 'labelClass', 'icon', 'iconClass', 'avatar', 'badge', 'click'])"
:class="[ui.base, ui.padding, ui.width, ui.ring, ui.rounded, ui.font, ui.size]"
:active-class="ui.active"
:inactive-class="ui.inactive"
@@ -22,11 +22,11 @@
<UIcon
v-if="link.icon"
:name="link.icon"
:class="[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 :link="link" :is-active="isActive">
<span v-if="link.label" :class="ui.label">{{ link.label }}</span>
<span v-if="link.label" :class="twMerge(ui.label, link.labelClass)">{{ link.label }}</span>
</slot>
<slot name="badge" :link="link" :is-active="isActive">
<span v-if="link.badge" :class="[ui.badge.base, isActive ? ui.badge.active : ui.badge.inactive]">
@@ -40,11 +40,12 @@
<script lang="ts">
import { toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import ULink from '../elements/Link.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig, omit } from '../../utils'
import { mergeConfig, twMerge, omit } from '../../utils'
import type { VerticalNavigationLink, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
@@ -80,7 +81,9 @@ export default defineComponent({
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
omit
omit,
twMerge,
twJoin
}
}
})

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@
</template>
<script lang="ts">
import { computed, ref, toRef, onMounted, defineComponent } from 'vue'
import { computed, ref, toRef, onMounted, defineComponent, watch } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { Popover as HPopover, PopoverButton as HPopoverButton, PopoverPanel as HPopoverPanel } from '@headlessui/vue'
@@ -94,8 +94,8 @@ export default defineComponent({
default: () => ({})
}
},
emits: ['update:open'],
setup (props) {
emits: ['update:open', 'open', 'close'],
setup (props, { emit }) {
const { ui, attrs } = useUI('popover', toRef(props, 'ui'), config, toRef(props, 'class'))
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
@@ -170,6 +170,11 @@ export default defineComponent({
}, props.closeDelay)
}
watch(() => popoverApi.value?.popoverState, (newValue: number, oldValue: number) => {
if (oldValue === undefined) return
emit(newValue === 0 ? 'open' : 'close')
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -2,6 +2,7 @@ import type { Link } from './link'
export interface BreadcrumbLink extends Link {
label: string
labelClass?: string
icon?: string
iconClass?: string
// FIXME: This is a workaround for `link.to` not being resolved although it extends `NuxtLinkProps`

View File

@@ -3,6 +3,7 @@ import type { Avatar } from './avatar'
export interface DropdownItem extends NuxtLinkProps {
label: string
labelClass?: string
slot?: string
icon?: string
iconClass?: string

View File

@@ -3,6 +3,7 @@ import type { Avatar } from './avatar'
export interface VerticalNavigationLink extends Link {
label: string
labelClass?: string
icon?: string
iconClass?: string
avatar?: Avatar

View File

@@ -76,24 +76,24 @@ export default {
wrapper: 'absolute inset-y-0 start-0 flex items-center',
pointer: 'pointer-events-none',
padding: {
'2xs': 'ps-2',
xs: 'ps-2.5',
sm: 'ps-2.5',
md: 'ps-3',
lg: 'ps-3.5',
xl: 'ps-3.5'
'2xs': 'px-2',
xs: 'px-2.5',
sm: 'px-2.5',
md: 'px-3',
lg: 'px-3.5',
xl: 'px-3.5'
}
},
trailing: {
wrapper: 'absolute inset-y-0 end-0 flex items-center',
pointer: 'pointer-events-none',
padding: {
'2xs': 'pe-2',
xs: 'pe-2.5',
sm: 'pe-2.5',
md: 'pe-3',
lg: 'pe-3.5',
xl: 'pe-3.5'
'2xs': 'px-2',
xs: 'px-2.5',
sm: 'px-2.5',
md: 'px-3',
lg: 'px-3.5',
xl: 'px-3.5'
}
}
},

View File

@@ -0,0 +1,63 @@
import { arrow } from '../popper'
export default {
container: 'z-20 group',
trigger: 'inline-flex w-full',
width: 'w-full',
height: 'max-h-60',
base: 'relative focus:outline-none overflow-y-auto scroll-py-1',
background: 'bg-white dark:bg-gray-800',
shadow: 'shadow-lg',
rounded: 'rounded-md',
padding: 'p-1',
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
option: {
base: 'cursor-default select-none relative flex items-center justify-between gap-1',
rounded: 'rounded-md',
padding: 'px-2 py-1.5',
size: 'text-sm',
color: 'text-gray-900 dark:text-white',
container: 'flex items-center gap-2 min-w-0',
active: 'bg-gray-100 dark:bg-gray-900',
inactive: '',
selected: 'pe-7',
disabled: 'cursor-not-allowed opacity-50',
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
icon: {
base: 'flex-shrink-0 h-4 w-4',
active: 'text-gray-900 dark:text-white',
inactive: 'text-gray-400 dark:text-gray-500'
},
selectedIcon: {
wrapper: 'absolute inset-y-0 end-0 flex items-center',
padding: 'pe-2',
base: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
},
avatar: {
base: 'flex-shrink-0',
size: '3xs' as const
},
chip: {
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
}
},
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
transition: {
leaveActiveClass: 'transition ease-in duration-100',
leaveFromClass: 'opacity-100',
leaveToClass: 'opacity-0'
},
popper: {
placement: 'bottom-end'
},
default: {
selectedIcon: 'i-heroicons-check-20-solid',
trailingIcon: 'i-heroicons-chevron-down-20-solid'
},
arrow: {
...arrow,
ring: 'before:ring-1 before:ring-gray-200 dark:before:ring-gray-700',
background: 'before:bg-white dark:before:bg-gray-700'
}
}

View File

@@ -1,50 +1,15 @@
import { arrow } from '../popper'
import inputMenu from './inputMenu'
export default {
container: 'z-20 group',
trigger: 'inline-flex w-full',
...inputMenu,
select: 'inline-flex items-center text-left cursor-default',
width: 'w-full',
height: 'max-h-60',
base: 'relative focus:outline-none overflow-y-auto scroll-py-1',
background: 'bg-white dark:bg-gray-800',
shadow: 'shadow-lg',
rounded: 'rounded-md',
padding: 'p-1',
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
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',
label: 'block truncate',
option: {
base: 'cursor-default select-none relative flex items-center justify-between gap-1',
rounded: 'rounded-md',
padding: 'px-2 py-1.5',
size: 'text-sm',
color: 'text-gray-900 dark:text-white',
container: 'flex items-center gap-2 min-w-0',
active: 'bg-gray-100 dark:bg-gray-900',
inactive: '',
selected: 'pe-7',
disabled: 'cursor-not-allowed opacity-50',
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
create: 'block truncate',
icon: {
base: 'flex-shrink-0 h-4 w-4',
active: 'text-gray-900 dark:text-white',
inactive: 'text-gray-400 dark:text-gray-500'
},
selectedIcon: {
wrapper: 'absolute inset-y-0 end-0 flex items-center',
padding: 'pe-2',
base: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
},
avatar: {
base: 'flex-shrink-0',
size: '3xs' as const
},
chip: {
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
}
...inputMenu.option,
create: 'block truncate'
},
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
transition: {

View File

@@ -18,6 +18,7 @@ export { default as meterGroup } from './elements/meterGroup'
// Forms
export { default as input } from './forms/input'
export { default as inputMenu } from './forms/inputMenu'
export { default as formGroup } from './forms/formGroup'
export { default as textarea } from './forms/textarea'
export { default as select } from './forms/select'

View File

@@ -3,6 +3,7 @@ export default {
ol: 'flex items-center gap-x-1.5',
li: 'flex items-center gap-x-1.5 text-gray-500 dark:text-gray-400 text-sm',
base: 'flex items-center gap-x-1.5 group font-semibold',
label: '',
icon: {
base: 'flex-shrink-0 w-4 h-4',
active: '',

View File

@@ -4,5 +4,6 @@ export const arrow = {
rounded: 'before:rounded-sm',
background: 'before:bg-gray-200 dark:before:bg-gray-800',
shadow: 'before:shadow',
placement: 'group-data-[popper-placement*="right"]:-left-1 group-data-[popper-placement*="left"]:-right-1 group-data-[popper-placement*="top"]:-bottom-1 group-data-[popper-placement*="bottom"]:-top-1'
// eslint-disable-next-line quotes
placement: `group-data-[popper-placement*='right']:-left-1 group-data-[popper-placement*='left']:-right-1 group-data-[popper-placement*='top']:-bottom-1 group-data-[popper-placement*='bottom']:-top-1`
}

View File

@@ -1,17 +1,21 @@
import { defu, createDefu } from 'defu'
import { extendTailwindMerge } from 'tailwind-merge'
import type { Strategy } from '../types'
// @ts-ignore
import appConfig from '#build/app.config'
const customTwMerge = extendTailwindMerge({
classGroups: {
icons: [(classPart: string) => /^i-/.test(classPart)]
export const twMerge = extendTailwindMerge<string, string>(defu({
extend: {
classGroups: {
icons: [(classPart: string) => /^i-/.test(classPart)]
}
}
})
}, appConfig.ui?.tailwindMerge))
const defuTwMerge = createDefu((obj, key, value, namespace) => {
if (namespace !== 'default' && !namespace.startsWith('default.') && typeof obj[key] === 'string' && typeof value === 'string' && obj[key] && value) {
// @ts-ignore
obj[key] = customTwMerge(obj[key], value)
obj[key] = twMerge(obj[key], value)
return true
}
})