mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-28 02:40:35 +01:00
feat(InputMenu): new component (#1095)
This commit is contained in:
@@ -10,6 +10,7 @@ const options = [
|
|||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
input: undefined,
|
input: undefined,
|
||||||
|
inputMenu: undefined,
|
||||||
textarea: undefined,
|
textarea: undefined,
|
||||||
select: undefined,
|
select: undefined,
|
||||||
selectMenu: undefined,
|
selectMenu: undefined,
|
||||||
@@ -23,6 +24,9 @@ const state = reactive({
|
|||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
input: z.string().min(10),
|
input: z.string().min(10),
|
||||||
|
inputMenu: z.any().refine(option => option?.value === 'option-2', {
|
||||||
|
message: 'Select Option 2'
|
||||||
|
}),
|
||||||
textarea: z.string().min(10),
|
textarea: z.string().min(10),
|
||||||
select: z.string().refine(value => value === 'option-2', {
|
select: z.string().refine(value => value === 'option-2', {
|
||||||
message: 'Select Option 2'
|
message: 'Select Option 2'
|
||||||
@@ -61,6 +65,10 @@ async function onSubmit (event: FormSubmitEvent<Schema>) {
|
|||||||
<UInput v-model="state.input" />
|
<UInput v-model="state.input" />
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup name="inputMenu" label="Input Menu">
|
||||||
|
<UInputMenu v-model="state.inputMenu" :options="options" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup name="textarea" label="Textarea">
|
<UFormGroup name="textarea" label="Textarea">
|
||||||
<UTextarea v-model="state.textarea" />
|
<UTextarea v-model="state.textarea" />
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
36
docs/components/content/examples/InputMenuExampleObjects.vue
Normal file
36
docs/components/content/examples/InputMenuExampleObjects.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -172,13 +172,13 @@ Use the `#leading` slot to set the content of the leading icon.
|
|||||||
::component-card
|
::component-card
|
||||||
---
|
---
|
||||||
slots:
|
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:
|
baseProps:
|
||||||
placeholder: 'Search...'
|
placeholder: 'Search...'
|
||||||
---
|
---
|
||||||
|
|
||||||
#leading
|
#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`
|
### `trailing`
|
||||||
|
|||||||
170
docs/content/3.forms/2.input-menu.md
Normal file
170
docs/content/3.forms/2.input-menu.md
Normal 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)"}
|
||||||
|
::
|
||||||
@@ -203,7 +203,7 @@ Use the `#leading` slot to set the content of the leading icon.
|
|||||||
::component-card
|
::component-card
|
||||||
---
|
---
|
||||||
slots:
|
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:
|
baseProps:
|
||||||
options:
|
options:
|
||||||
- 'United States'
|
- 'United States'
|
||||||
@@ -213,7 +213,7 @@ baseProps:
|
|||||||
---
|
---
|
||||||
|
|
||||||
#leading
|
#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`
|
### `trailing`
|
||||||
|
|||||||
411
src/runtime/components/forms/InputMenu.vue
Normal file
411
src/runtime/components/forms/InputMenu.vue
Normal 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 { twMerge, 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 { 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>
|
||||||
@@ -76,24 +76,24 @@ export default {
|
|||||||
wrapper: 'absolute inset-y-0 start-0 flex items-center',
|
wrapper: 'absolute inset-y-0 start-0 flex items-center',
|
||||||
pointer: 'pointer-events-none',
|
pointer: 'pointer-events-none',
|
||||||
padding: {
|
padding: {
|
||||||
'2xs': 'ps-2',
|
'2xs': 'px-2',
|
||||||
xs: 'ps-2.5',
|
xs: 'px-2.5',
|
||||||
sm: 'ps-2.5',
|
sm: 'px-2.5',
|
||||||
md: 'ps-3',
|
md: 'px-3',
|
||||||
lg: 'ps-3.5',
|
lg: 'px-3.5',
|
||||||
xl: 'ps-3.5'
|
xl: 'px-3.5'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailing: {
|
trailing: {
|
||||||
wrapper: 'absolute inset-y-0 end-0 flex items-center',
|
wrapper: 'absolute inset-y-0 end-0 flex items-center',
|
||||||
pointer: 'pointer-events-none',
|
pointer: 'pointer-events-none',
|
||||||
padding: {
|
padding: {
|
||||||
'2xs': 'pe-2',
|
'2xs': 'px-2',
|
||||||
xs: 'pe-2.5',
|
xs: 'px-2.5',
|
||||||
sm: 'pe-2.5',
|
sm: 'px-2.5',
|
||||||
md: 'pe-3',
|
md: 'px-3',
|
||||||
lg: 'pe-3.5',
|
lg: 'px-3.5',
|
||||||
xl: 'pe-3.5'
|
xl: 'px-3.5'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
63
src/runtime/ui.config/forms/inputMenu.ts
Normal file
63
src/runtime/ui.config/forms/inputMenu.ts
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +1,15 @@
|
|||||||
import { arrow } from '../popper'
|
import { arrow } from '../popper'
|
||||||
|
import inputMenu from './inputMenu'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
container: 'z-20 group',
|
...inputMenu,
|
||||||
trigger: 'inline-flex w-full',
|
|
||||||
select: 'inline-flex items-center text-left cursor-default',
|
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',
|
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',
|
||||||
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
|
|
||||||
option: {
|
option: {
|
||||||
base: 'cursor-default select-none relative flex items-center justify-between gap-1',
|
...inputMenu.option,
|
||||||
rounded: 'rounded-md',
|
create: 'block truncate'
|
||||||
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'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
|
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
|
||||||
transition: {
|
transition: {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export { default as meterGroup } from './elements/meterGroup'
|
|||||||
|
|
||||||
// Forms
|
// Forms
|
||||||
export { default as input } from './forms/input'
|
export { default as input } from './forms/input'
|
||||||
|
export { default as inputMenu } from './forms/inputMenu'
|
||||||
export { default as formGroup } from './forms/formGroup'
|
export { default as formGroup } from './forms/formGroup'
|
||||||
export { default as textarea } from './forms/textarea'
|
export { default as textarea } from './forms/textarea'
|
||||||
export { default as select } from './forms/select'
|
export { default as select } from './forms/select'
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ export const arrow = {
|
|||||||
rounded: 'before:rounded-sm',
|
rounded: 'before:rounded-sm',
|
||||||
background: 'before:bg-gray-200 dark:before:bg-gray-800',
|
background: 'before:bg-gray-200 dark:before:bg-gray-800',
|
||||||
shadow: 'before:shadow',
|
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`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user