Compare commits

..

5 Commits

Author SHA1 Message Date
rdjanuar
21939ed333 docs(SelectMenu): add section customization on clearable prop 2024-11-12 19:04:42 +07:00
rdjanuar
5a414eb55a feat(SelectMenu): add clearble 2024-11-12 18:47:39 +07:00
kyyy
3a5960fb58 Merge branch 'dev' into issue-1057 2024-11-12 17:17:46 +07:00
rdjanuar
a78203ce49 up 2024-11-08 14:30:53 +07:00
rdjanuar
592da565fe feat(SelectMenu): add clearble 2024-11-08 14:02:54 +07:00
50 changed files with 2701 additions and 2430 deletions

View File

@@ -29,20 +29,11 @@ body:
- Build Modules: `-` - Build Modules: `-`
validations: validations:
required: true required: true
- type: dropdown
id: package
attributes:
label: Is this bug related to Nuxt or Vue?
options:
- Nuxt
- Vue
validations:
required: true
- type: input - type: input
id: version id: version
attributes: attributes:
label: Version label: Version
placeholder: v3.0.0-alpha.x placeholder: v3.0.0-alpha.5
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@@ -12,7 +12,7 @@ body:
label: For what version of Nuxt UI are you suggesting this? label: For what version of Nuxt UI are you suggesting this?
options: options:
- v2.x - v2.x
- v3.0.0-alpha.x - v3-alpha
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@@ -12,7 +12,7 @@ body:
label: For what version of Nuxt UI are you asking this question? label: For what version of Nuxt UI are you asking this question?
options: options:
- v2.x - v2.x
- v3.0.0-alpha.x - v3-alpha
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@@ -1,38 +1,5 @@
# Changelog # Changelog
## [2.20.0](https://github.com/nuxt/ui/compare/v2.19.2...v2.20.0) (2024-12-09)
### ⚠ BREAKING CHANGES
* **Form:** resolve async validation in yup & issue directly mutate state (#2701)
### Features
* **Accordion:** add `close` event ([#2750](https://github.com/nuxt/ui/issues/2750)) ([419a24f](https://github.com/nuxt/ui/commit/419a24f7034cefda2c6669f3c26742552e500f63))
* **Badge:** handle `icon` prop ([#2594](https://github.com/nuxt/ui/issues/2594)) ([0d1a76e](https://github.com/nuxt/ui/commit/0d1a76e3c69e08534abb295b96548e67cfbea00c))
* **InputMenu/SelectMenu:** add support for `dot notation` in `by` prop ([#2607](https://github.com/nuxt/ui/issues/2607)) ([53df9d9](https://github.com/nuxt/ui/commit/53df9d9a8cd6850803bdafc7ef6efe4e7404d334))
* **Link:** allow partial query match for `activeClass` ([#2663](https://github.com/nuxt/ui/issues/2663)) ([03e24f4](https://github.com/nuxt/ui/commit/03e24f45836bdddd94b30cbaecc2288a78b56b0b))
* **Notification:** add `pauseTimeoutOnHover` prop ([#2661](https://github.com/nuxt/ui/issues/2661)) ([11b8c3d](https://github.com/nuxt/ui/commit/11b8c3d9db1ec62b1c3557703c7ab5c99cb42df5))
* **Table:** add contextmenu handling to table rows ([#2283](https://github.com/nuxt/ui/issues/2283)) ([c9e6256](https://github.com/nuxt/ui/commit/c9e6256e7f2c06da8bfda13700f56f6994e76eab))
* **Table:** add custom `[@select](https://github.com/select):all` event ([#2581](https://github.com/nuxt/ui/issues/2581)) ([ac323c4](https://github.com/nuxt/ui/commit/ac323c4cccd930f2cd8c1f54b325bd509acd40bf))
* **Table:** allow dynamically render `checkbox` ([#2549](https://github.com/nuxt/ui/issues/2549)) ([d6daf46](https://github.com/nuxt/ui/commit/d6daf466ace42b828151c45b18cd47179e85d66d))
### Bug Fixes
* **AvatarGroup/ButtonGroup/MeterGroup:** allow deeply partial `ui` config ([#2542](https://github.com/nuxt/ui/issues/2542)) ([bf58086](https://github.com/nuxt/ui/commit/bf580863af11d6a1a4c6c6774b44ec37b082e933))
* **Carousel:** wrong `ui` type with `strategy` ([07ef771](https://github.com/nuxt/ui/commit/07ef771b17c72e275508a273371454a5e8a62257))
* **components:** replace `as const` with correct type in config ([#2652](https://github.com/nuxt/ui/issues/2652)) ([51c8b8e](https://github.com/nuxt/ui/commit/51c8b8e3e59d7eceff72625650a199fcf7c6feca))
* **date-picker:** undefined `dayIndex` ([#2545](https://github.com/nuxt/ui/issues/2545)) ([ce955d2](https://github.com/nuxt/ui/commit/ce955d24f1dfd222e87ce88428c0612c3f13cd50))
* **Form:** resolve async validation in yup & issue directly mutate state ([#2701](https://github.com/nuxt/ui/issues/2701)) ([f3632dd](https://github.com/nuxt/ui/commit/f3632ddee511f0fccb24d4fc37403421e84ffdae))
* **Form:** use parsed value from `joi` instead of original state ([#2587](https://github.com/nuxt/ui/issues/2587)) ([acecff4](https://github.com/nuxt/ui/commit/acecff40ec0156e45b4934c5d10c4dfa7c135f8e))
* **InputMenu/SelectMenu:** use `by` prop to compare objects & support dot notation in `value-attribute` ([#2566](https://github.com/nuxt/ui/issues/2566)) ([7154254](https://github.com/nuxt/ui/commit/7154254ac22830f651ec200f7f3af2f5577f2de0))
* **Link:** `exactQuery` prop type ([#2781](https://github.com/nuxt/ui/issues/2781)) ([4cde571](https://github.com/nuxt/ui/commit/4cde571e387775a9b12759f6f8c99117c84cbcff))
* **Notification:** element renders even when no `notification` is present ([#2561](https://github.com/nuxt/ui/issues/2561)) ([d4e408c](https://github.com/nuxt/ui/commit/d4e408cfd8e2ef26021519f2f30f57e9120e1939))
* **Table:** data outdated when rows change ([#2600](https://github.com/nuxt/ui/issues/2600)) ([b23f2de](https://github.com/nuxt/ui/commit/b23f2decfc9607555a315d0d087d0a042f03a938))
* **Table:** missing type on props `loadingState` ([#2551](https://github.com/nuxt/ui/issues/2551)) ([6e66990](https://github.com/nuxt/ui/commit/6e66990372ef6bd7c109a64c753d9b50e96a450b))
* **Table:** prevent `onClick` while blocking element ([#2592](https://github.com/nuxt/ui/issues/2592)) ([9703786](https://github.com/nuxt/ui/commit/97037864b39749db228fa5f51981f19e4a9c29dd))
* **types:** improve `DeepPartial` type for App Config ([#2621](https://github.com/nuxt/ui/issues/2621)) ([976b03f](https://github.com/nuxt/ui/commit/976b03f241ef9626a6338685e43c844a8b3953fd))
## [2.19.2](https://github.com/nuxt/ui/compare/v2.19.1...v2.19.2) (2024-11-05) ## [2.19.2](https://github.com/nuxt/ui/compare/v2.19.1...v2.19.2) (2024-11-05)
### Bug Fixes ### Bug Fixes

View File

@@ -1,4 +1,4 @@
[![nuxt-ui.png](https://volta.s3.fr-par.scw.cloud/nuxt_ui_social_card_531d133fa2.png)](https://ui.nuxt.com) [![nuxt-ui.png](https://repository-images.githubusercontent.com/428329515/43fec891-9030-4601-8233-5d45ba5c6013)](https://ui.nuxt.com)
# Nuxt UI # Nuxt UI

View File

@@ -3,7 +3,7 @@
<div> <div>
<NuxtLoadingIndicator /> <NuxtLoadingIndicator />
<!-- <Banner v-if="!$route.path.startsWith('/examples')" /> --> <Banner v-if="!$route.path.startsWith('/examples')" />
<Header v-if="!$route.path.startsWith('/examples')" :links="links" /> <Header v-if="!$route.path.startsWith('/examples')" :links="links" />

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const id = 'nuxt-ui-banner-3' const id = 'nuxt-ui-banner-2'
const to = '/pro/pricing' const to = 'https://ui3.nuxt.dev'
const hideBanner = () => { const hideBanner = () => {
localStorage.setItem(id, 'true') localStorage.setItem(id, 'true')
@@ -28,8 +28,9 @@ if (import.meta.server) {
<NuxtLink <NuxtLink
v-if="to" v-if="to"
:to="to" :to="to"
target="_blank"
class="focus:outline-none" class="focus:outline-none"
aria-label="20% off on all Nuxt UI Pro products for Black Friday week" aria-label="Nuxt UI Pro pricing"
tabindex="-1" tabindex="-1"
> >
<span class="absolute inset-0 " aria-hidden="true" /> <span class="absolute inset-0 " aria-hidden="true" />
@@ -39,19 +40,19 @@ if (import.meta.server) {
<div class="lg:flex-1 hidden lg:flex items-center" /> <div class="lg:flex-1 hidden lg:flex items-center" />
<p class="text-sm font-medium text-white dark:text-gray-900 truncate"> <p class="text-sm font-medium text-white dark:text-gray-900 truncate">
<UIcon name="i-ri-discount-percent-fill" class="size-5 align-top flex-shrink-0 pointer-events-none mr-2" /> <UIcon name="i-heroicons-rocket-launch" class="w-5 h-5 align-top flex-shrink-0 pointer-events-none mr-2" />
<span class="font-bold">Black Friday Week</span>: <UBadge label="20% off" color="white" class="ring-0 font-semibold" /> on all Nuxt UI Pro products from <span class="font-semibold">Nov 25</span> to <span class="font-semibold">Dec 2</span>! <span class="font-semibold">Nuxt UI v3-alpha</span> has been released!
</p> </p>
<!-- <UButton <UButton
:to="to" to="https://ui3.nuxt.dev"
target="_blank" target="_blank"
label="Buy now" label="Try it out"
color="black" color="black"
variant="solid" variant="solid"
size="2xs" size="2xs"
trailing-icon="i-heroicons-arrow-right-20-solid" trailing-icon="i-heroicons-arrow-right-20-solid"
/> --> />
<div class="flex items-center justify-end lg:flex-1"> <div class="flex items-center justify-end lg:flex-1">
<button <button

View File

@@ -10,34 +10,12 @@
}" }"
> >
<template #left> <template #left>
<NuxtLink to="/" class="flex items-end gap-2 text-xl text-gray-900 dark:text-white min-w-0 shrink-0" aria-label="Nuxt UI"> <NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-gray-900 dark:text-white min-w-0" aria-label="Nuxt UI">
<LogoPro v-if="$route.path.startsWith('/pro')" class="w-auto h-6 shrink-0" /> <LogoPro v-if="$route.path.startsWith('/pro')" class="w-auto h-6 shrink-0" />
<Logo v-else class="w-auto h-6 shrink-0" /> <Logo v-else class="w-auto h-6 shrink-0" />
</NuxtLink>
<UDropdown <UBadge :label="$route.path.startsWith('/pro') ? `v${pkg.version.split('-')[0]}` : `v${config.version}`" variant="subtle" size="xs" class="-mb-[2px] rounded font-semibold truncate hidden sm:inline-flex" />
:items="[[{ label: $route.path.startsWith('/pro') ? `v${pkg.version.split('-')[0]}` : `v${config.version}`, class: 'text-primary-500 dark:text-primary-400' }, { label: 'v3.0.0-alpha.x', to: 'https://ui3.nuxt.dev' }]]" </NuxtLink>
:popper="{ strategy: 'absolute', offsetDistance: 11, placement: 'bottom-start' }"
:ui="{
background: 'dark:bg-gray-900',
ring: 'dark:ring-gray-800',
width: 'w-auto',
item: {
padding: 'p-1',
size: 'text-xs',
active: 'dark:bg-gray-800/50'
}
}"
>
<UButton
:label="$route.path.startsWith('/pro') ? `v${pkg.version.split('-')[0]}` : `v${config.version}`"
trailing-icon="i-lucide-chevron-down"
variant="outline"
size="2xs"
truncate
class="-mb-[6px] font-semibold rounded-full truncate ring-primary-500/25 dark:ring-primary-400/25 bg-primary-500/10 dark:bg-primary-400/10 hover:bg-primary-500/15 dark:hover:bg-primary-400/15 transition-colors"
/>
</UDropdown>
</template> </template>
<template #right> <template #right>
@@ -47,10 +25,10 @@
<UContentSearchButton :label="null" /> <UContentSearchButton :label="null" />
</UTooltip> </UTooltip>
<UColorModeButton class="hidden lg:inline-flex" /> <UColorModeButton />
<UButton <UButton
to="https://github.com/nuxt/ui/tree/dev" to="https://github.com/nuxt/ui"
target="_blank" target="_blank"
icon="i-simple-icons-github" icon="i-simple-icons-github"
aria-label="GitHub" aria-label="GitHub"

View File

@@ -45,7 +45,7 @@ const ui = {
inactive: 'text-gray-400 dark:text-gray-500' inactive: 'text-gray-400 dark:text-gray-500'
}, },
avatar: { avatar: {
size: '2xs' size: '2xs' as const
} }
} }
} }

View File

@@ -0,0 +1,102 @@
<script setup lang="ts">
const options = ref([
{ id: 1, name: 'bug', color: 'd73a4a' },
{ id: 2, name: 'documentation', color: '0075ca' },
{ id: 3, name: 'duplicate', color: 'cfd3d7' },
{ id: 4, name: 'enhancement', color: 'a2eeef' },
{ id: 5, name: 'good first issue', color: '7057ff' },
{ id: 6, name: 'help wanted', color: '008672' },
{ id: 7, name: 'invalid', color: 'e4e669' },
{ id: 8, name: 'question', color: 'd876e3' },
{ id: 9, name: 'wontfix', color: 'ffffff' }
])
const selected = ref([])
const labels = computed({
get: () => selected.value,
set: async (labels) => {
const promises = labels.map(async (label) => {
if (label.id) {
return label
}
// In a real app, you would make an API call to create the label
const response = {
id: options.value.length + 1,
name: label.name,
color: generateColorFromString(label.name)
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
function hashCode(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return hash
}
function intToRGB(i) {
const c = (i & 0x00FFFFFF)
.toString(16)
.toUpperCase()
return '00000'.substring(0, 6 - c.length) + c
}
function generateColorFromString(str) {
return intToRGB(hashCode(str))
}
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
clearable
multiple
searchable
creatable
>
<template #label>
<template v-if="labels.length">
<span class="flex items-center -space-x-1">
<span v-for="label of labels" :key="label.id" class="flex-shrink-0 w-2 h-2 mt-px rounded-full" :style="{ background: `#${label.color}` }" />
</span>
<span>{{ labels.length }} label{{ labels.length > 1 ? 's' : '' }}</span>
</template>
<template v-else>
<span class="text-gray-500 dark:text-gray-400 truncate">Select labels</span>
</template>
</template>
<template #option="{ option }">
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full"
:style="{ background: `#${option.color}` }"
/>
<span class="truncate">{{ option.name }}</span>
</template>
<template #option-create="{ option }">
<span class="flex-shrink-0">New label:</span>
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full -mx-1"
:style="{ background: `#${generateColorFromString(option.name)}` }"
/>
<span class="block truncate">{{ option.name }}</span>
</template>
</USelectMenu>
</template>

View File

@@ -0,0 +1,105 @@
<script setup lang="ts">
const options = ref([
{ id: 1, name: 'bug', color: 'd73a4a' },
{ id: 2, name: 'documentation', color: '0075ca' },
{ id: 3, name: 'duplicate', color: 'cfd3d7' },
{ id: 4, name: 'enhancement', color: 'a2eeef' },
{ id: 5, name: 'good first issue', color: '7057ff' },
{ id: 6, name: 'help wanted', color: '008672' },
{ id: 7, name: 'invalid', color: 'e4e669' },
{ id: 8, name: 'question', color: 'd876e3' },
{ id: 9, name: 'wontfix', color: 'ffffff' }
])
const selected = ref([])
const labels = computed({
get: () => selected.value,
set: async (labels) => {
const promises = labels.map(async (label) => {
if (label.id) {
return label
}
// In a real app, you would make an API call to create the label
const response = {
id: options.value.length + 1,
name: label.name,
color: generateColorFromString(label.name)
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
function hashCode(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return hash
}
function intToRGB(i) {
const c = (i & 0x00FFFFFF)
.toString(16)
.toUpperCase()
return '00000'.substring(0, 6 - c.length) + c
}
function generateColorFromString(str) {
return intToRGB(hashCode(str))
}
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
clearable
multiple
searchable
creatable
>
<template #label>
<template v-if="labels.length">
<span class="flex items-center -space-x-1">
<span v-for="label of labels" :key="label.id" class="flex-shrink-0 w-2 h-2 mt-px rounded-full" :style="{ background: `#${label.color}` }" />
</span>
<span>{{ labels.length }} label{{ labels.length > 1 ? 's' : '' }}</span>
</template>
<template v-else>
<span class="text-gray-500 dark:text-gray-400 truncate">Select labels</span>
</template>
</template>
<template #option="{ option }">
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full"
:style="{ background: `#${option.color}` }"
/>
<span class="truncate">{{ option.name }}</span>
</template>
<template #option-create="{ option }">
<span class="flex-shrink-0">New label:</span>
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full -mx-1"
:style="{ background: `#${generateColorFromString(option.name)}` }"
/>
<span class="block truncate">{{ option.name }}</span>
</template>
<template #clearable="{ onClear }">
<UButton icon="i-heroicons-trash-20-solid" size="xs" class="text-gray-400 dark:text-gray-500" variant="ghost" @click.capture.stop="onClear" />
</template>
</USelectMenu>
</template>

View File

@@ -141,74 +141,6 @@ Badge
You can customize the whole [preset](#preset) by using the `ui` prop. You can customize the whole [preset](#preset) by using the `ui` prop.
:: ::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
::component-card
---
props:
icon: 'i-heroicons-rocket-launch'
size: 'sm'
color: 'primary'
variant: 'solid'
label: Badge
trailing: false
options:
- name: variant
restriction: only
values:
- solid
excludedProps:
- icon
- label
---
::
## Slots
### `leading`
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" />
baseProps:
color: 'gray'
props:
label: Badge
color: 'gray'
excludedProps:
- color
---
#leading
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
::
### `trailing`
Use the `#trailing` slot to set the content of the trailing icon.
::component-card
---
slots:
trailing: <UIcon name="i-heroicons-rocket-launch" class="w-4 h-4" />
props:
label: Badge
color: 'gray'
excludedProps:
- color
---
#trailing
:u-icon{name="i-heroicons-rocket-launch" class="w-4 h-4"}
::
## Props ## Props
:component-props :component-props

View File

@@ -142,7 +142,7 @@ props:
### Loading ### Loading
Use the `loading` prop to show a loading icon in the Input. Use the `loading` prop to show a loading icon and disable the Input.
Use the `loading-icon` prop to set a different icon or change it globally in `ui.input.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`. Use the `loading-icon` prop to set a different icon or change it globally in `ui.input.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.

View File

@@ -14,7 +14,6 @@ The Link component is a wrapper around [`<NuxtLink>`](https://nuxt.com/docs/api/
- `inactive-class` prop to set a class when the link is inactive, `active-class` is used when active. - `inactive-class` prop to set a class when the link is inactive, `active-class` is used when active.
- `exact` prop to style with `active-class` when the link is active and the route is exactly the same as the current route. - `exact` prop to style with `active-class` when the link is active and the route is exactly the same as the current route.
- `exact-query` and `exact-hash` props to style with `active-class` when the link is active and the query or hash is exactly the same as the current query or hash. - `exact-query` and `exact-hash` props to style with `active-class` when the link is active and the query or hash is exactly the same as the current query or hash.
- use `exact-query="partial"` to style with `active-class` when the link is active and the query partially match the current query.
The incentive behind this is to provide the same API as NuxtLink back in Nuxt 2 / Vue 2. You can read more about it in the Vue Router [migration from Vue 2](https://router.vuejs.org/guide/migration/#removal-of-the-exact-prop-in-router-link) guide. The incentive behind this is to provide the same API as NuxtLink back in Nuxt 2 / Vue 2. You can read more about it in the Vue Router [migration from Vue 2](https://router.vuejs.org/guide/migration/#removal-of-the-exact-prop-in-router-link) guide.

View File

@@ -137,9 +137,9 @@ excludedProps:
### Timeout ### Timeout
Use the `timeout` prop to configure how long the Notification will remain. The default value is `5000`, set it to `0` to disable the timeout. The `pauseTimeoutOnHover` prop (`true` by default) controls whether hovering the notification should pause the timeout. Use the `timeout` prop to configure how long the Notification will remain. The default value is `5000`, set it to `0` to disable the timeout.
You will see a progress bar at the bottom of the Notification which will indicate the remaining time. When hovering the Notification, the progress bar will be paused if `pauseTimeoutOnHover` is enabled; otherwise, it won't stop. You will see a progress bar at the bottom of the Notification which will indicate the remaining time. When hovering the Notification, the progress bar will be paused.
::component-card ::component-card
--- ---
@@ -149,7 +149,6 @@ baseProps:
description: 'This is a notification.' description: 'This is a notification.'
props: props:
timeout: 60000 timeout: 60000
pauseTimeoutOnHover: true
--- ---
:: ::

View File

@@ -156,7 +156,38 @@ Use the `searchableLazy` prop to control the immediacy of data requests.
--- ---
component: 'select-menu-example-search-async' component: 'select-menu-example-search-async'
componentProps: componentProps:
class: 'w-full lg:w-48' class: 'w-full lg:w-48'
---
::
## Clearable
The `clearable` prop allows users to easily remove their selected option(s) with a clear button.
::component-example
---
component: 'select-menu-example-clearable'
componentProps:
class: 'w-full lg:w-52'
---
::
### Customization
#### Slot Props
The slot provides four key props:
| Prop | Type | Description |
|------|------|-------------|
| `selected` | `Object` | The currently selected value/item in the component |
| `disabled` | `Boolean` | Whether the component is in a disabled state |
| `loading` | `Boolean` | Whether the component is in a loading state |
| `onClear` | `Function` | Callback function to clear the selected value when the clear button is clicked |
::component-example
---
component: 'select-menu-example-clearable-customization'
componentProps:
class: 'w-full lg:w-52'
--- ---
:: ::

View File

@@ -62,7 +62,7 @@ extraClass: 'overflow-hidden'
padding: false padding: false
component: 'table-example-columns-selectable' component: 'table-example-columns-selectable'
componentProps: componentProps:
class: 'flex-1 flex-col overflow-hidden min-h-[230px]' class: 'flex-1 flex-col overflow-hidden'
--- ---
:: ::
@@ -346,7 +346,7 @@ componentProps:
### Contextmenu ### Contextmenu
Use the `contextmenu` listener on your Table to make the rows right-clickable. The function will receive the original event as the first argument and the row as the second argument. Use the `contextmenu` listener on your Table to make the rows righ-clickable. The function will receive the original event as the first argument and the row as the second argument.
You can use this to open a [ContextMenu](/components/context-menu) for that row. You can use this to open a [ContextMenu](/components/context-menu) for that row.

View File

@@ -27,7 +27,8 @@ export default defineNuxtConfig({
'@nuxtjs/plausible', '@nuxtjs/plausible',
'@vueuse/nuxt', '@vueuse/nuxt',
'nuxt-component-meta', 'nuxt-component-meta',
'nuxt-cloudflare-analytics' 'nuxt-cloudflare-analytics',
'modules/content-examples-code'
], ],
site: { site: {

View File

@@ -4,27 +4,27 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@iconify-json/heroicons": "^1.2.1", "@iconify-json/heroicons": "^1.2.1",
"@iconify-json/simple-icons": "^1.2.14", "@iconify-json/simple-icons": "^1.2.11",
"@iconify-json/vscode-icons": "^1.2.3", "@iconify-json/vscode-icons": "^1.2.2",
"@nuxt/content": "^2.13.4", "@nuxt/content": "^2.13.4",
"@nuxt/fonts": "^0.10.3", "@nuxt/fonts": "^0.10.2",
"@nuxt/image": "^1.8.1", "@nuxt/image": "^1.8.1",
"@nuxt/ui": "latest", "@nuxt/ui": "latest",
"@nuxt/ui-pro": "^1.5.0", "@nuxt/ui-pro": "^1.5.0",
"@nuxtjs/plausible": "^1.2.0", "@nuxtjs/plausible": "^1.0.3",
"@octokit/rest": "^21.0.2", "@octokit/rest": "^21.0.2",
"@vueuse/nuxt": "^12.0.0", "@vueuse/nuxt": "^11.2.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"joi": "^17.13.3", "joi": "^17.13.3",
"nuxt": "^3.14.1592", "nuxt": "^3.14.159",
"nuxt-cloudflare-analytics": "^1.0.8", "nuxt-cloudflare-analytics": "^1.0.8",
"nuxt-component-meta": "^0.9.0", "nuxt-component-meta": "^0.9.0",
"nuxt-og-image": "^3.1.1", "nuxt-og-image": "^3.0.8",
"prettier": "^3.4.2", "prettier": "^3.3.3",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"v-calendar": "^3.1.2", "v-calendar": "^3.1.2",
"valibot": "^0.42.1", "valibot": "^0.42.1",
"yup": "^1.5.0", "yup": "^1.4.0",
"zod": "^3.23.8" "zod": "^3.23.8"
} }
} }

View File

@@ -1,8 +1,8 @@
{ {
"name": "@nuxt/ui", "name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.", "description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "2.20.0", "version": "2.19.2",
"packageManager": "pnpm@9.15.0", "packageManager": "pnpm@9.12.3",
"repository": "nuxt/ui", "repository": "nuxt/ui",
"homepage": "https://ui.nuxt.com", "homepage": "https://ui.nuxt.com",
"type": "module", "type": "module",
@@ -35,8 +35,8 @@
"@headlessui/tailwindcss": "^0.2.1", "@headlessui/tailwindcss": "^0.2.1",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@iconify-json/heroicons": "^1.2.1", "@iconify-json/heroicons": "^1.2.1",
"@nuxt/icon": "^1.9.1", "@nuxt/icon": "^1.7.2",
"@nuxt/kit": "^3.14.1592", "@nuxt/kit": "^3.14.159",
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/tailwindcss": "^6.12.2", "@nuxtjs/tailwindcss": "^6.12.2",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
@@ -44,45 +44,44 @@
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@vueuse/core": "^12.0.0", "@vueuse/core": "^11.2.0",
"@vueuse/integrations": "^12.0.0", "@vueuse/integrations": "^11.2.0",
"@vueuse/math": "^12.0.0", "@vueuse/math": "^11.2.0",
"defu": "^6.1.4", "defu": "^6.1.4",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"scule": "^1.3.0", "scule": "^1.3.0",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.16" "tailwindcss": "^3.4.14"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint-config": "^0.7.2", "@nuxt/eslint-config": "^0.6.1",
"@nuxt/module-builder": "^0.8.4", "@nuxt/module-builder": "^0.8.4",
"@nuxt/test-utils": "^3.15.1", "@nuxt/test-utils": "^3.14.4",
"@release-it/conventional-changelog": "^9.0.3", "@release-it/conventional-changelog": "^9.0.2",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"eslint": "^9.16.0", "eslint": "^9.14.0",
"happy-dom": "^14.12.3", "happy-dom": "^14.12.3",
"joi": "^17.13.3", "joi": "^17.13.3",
"nuxt": "^3.14.1592", "nuxt": "^3.14.159",
"release-it": "^17.10.0", "release-it": "^17.10.0",
"superstruct": "^2.0.2", "superstruct": "^2.0.2",
"typescript": "^5.6.3",
"unbuild": "^2.0.0", "unbuild": "^2.0.0",
"valibot": "^0.42.1", "valibot": "^0.42.1",
"valibot30": "npm:valibot@0.30.0", "valibot30": "npm:valibot@0.30.0",
"valibot31": "npm:valibot@0.31.0", "valibot31": "npm:valibot@0.31.0",
"vitest": "^2.1.8", "vitest": "^2.1.4",
"vitest-environment-nuxt": "^1.0.1", "vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.1.10", "vue-tsc": "^2.1.10",
"yup": "^1.5.0", "yup": "^1.4.0",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"resolutions": { "resolutions": {
"@nuxt/ui": "workspace:*", "@nuxt/ui": "workspace:*",
"@nuxt/content": "2.13.2", "@nuxt/content": "2.13.2",
"@nuxtjs/mdc": "0.9.0", "@nuxtjs/mdc": "0.9.0",
"chokidar": "3.6.0", "nuxt": "3.13.2",
"typescript": "5.6.3" "@nuxt/kit": "3.13.2"
} }
} }

View File

@@ -9,6 +9,6 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/ui": "latest", "@nuxt/ui": "latest",
"nuxt": "^3.14.1592" "nuxt": "^3.14.159"
} }
} }

4248
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -126,7 +126,7 @@ export default defineComponent({
default: () => ({}) default: () => ({})
} }
}, },
emits: ['open', 'close'], emits: ['open'],
setup(props, { emit }) { setup(props, { emit }) {
const { ui, attrs } = useUI('accordion', toRef(props, 'ui'), config, toRef(props, 'class')) const { ui, attrs } = useUI('accordion', toRef(props, 'ui'), config, toRef(props, 'class'))
@@ -142,8 +142,6 @@ export default defineComponent({
if (!isOpenBefore && isOpenAfter) { if (!isOpenBefore && isOpenAfter) {
emit('open', index) emit('open', index)
} else if (isOpenBefore && !isOpenAfter) {
emit('close', index)
} }
} }
}, { immediate: true }) }, { immediate: true })

View File

@@ -1,18 +1,6 @@
<template> <template>
<span :class="badgeClass" v-bind="attrs"> <span :class="badgeClass" v-bind="attrs">
<slot name="leading"> <slot>{{ label }}</slot>
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
</slot>
<slot>
<span v-if="label">
{{ label }}
</span>
</slot>
<slot name="trailing">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot>
</span> </span>
</template> </template>
@@ -20,7 +8,6 @@
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup' import { useInjectButtonGroup } from '../../composables/useButtonGroup'
@@ -32,9 +19,6 @@ import { badge } from '#ui/ui.config'
const config = mergeConfig<typeof badge>(appConfig.ui.strategy, appConfig.ui.badge, badge) const config = mergeConfig<typeof badge>(appConfig.ui.strategy, appConfig.ui.badge, badge)
export default defineComponent({ export default defineComponent({
components: {
UIcon
},
inheritAttrs: false, inheritAttrs: false,
props: { props: {
size: { size: {
@@ -65,26 +49,6 @@ export default defineComponent({
type: [String, Number], type: [String, Number],
default: null default: null
}, },
icon: {
type: String,
default: null
},
leadingIcon: {
type: String,
default: null
},
trailingIcon: {
type: String,
default: null
},
trailing: {
type: Boolean,
default: false
},
leading: {
type: Boolean,
default: false
},
class: { class: {
type: [String, Object, Array] as PropType<any>, type: [String, Object, Array] as PropType<any>,
default: () => '' default: () => ''
@@ -99,14 +63,6 @@ export default defineComponent({
const { size, rounded } = useInjectButtonGroup({ ui, props }) const { size, rounded } = useInjectButtonGroup({ ui, props })
const isLeading = computed(() => {
return (props.icon && props.leading) || (props.icon && !props.trailing) || !props.trailing || props.leadingIcon
})
const isTrailing = computed(() => {
return (props.icon && props.trailing) || props.trailing || props.trailingIcon
})
const badgeClass = computed(() => { const badgeClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant] const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -115,42 +71,13 @@ export default defineComponent({
ui.value.font, ui.value.font,
rounded.value, rounded.value,
ui.value.size[size.value], ui.value.size[size.value],
ui.value.gap[size.value],
variant?.replaceAll('{color}', props.color) variant?.replaceAll('{color}', props.color)
), props.class) ), props.class)
}) })
const leadingIconName = computed(() => {
return props.leadingIcon || props.icon
})
const trailingIconName = computed(() => {
return props.trailingIcon || props.icon
})
const leadingIconClass = computed(() => {
return twJoin(
ui.value.icon.base,
ui.value.icon.size[size.value]
)
})
const trailingIconClass = computed(() => {
return twJoin(
ui.value.icon.base,
ui.value.icon.size[size.value]
)
})
return { return {
attrs, attrs,
isLeading, badgeClass
isTrailing,
badgeClass,
leadingIconName,
trailingIconName,
leadingIconClass,
trailingIconClass
} }
} }
}) })

View File

@@ -106,7 +106,7 @@ export default defineComponent({
default: () => '' default: () => ''
}, },
ui: { ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>, type: Object as PropType<DeepPartial<typeof config & { strategy?: Strategy }>>,
default: undefined default: undefined
} }
}, },

View File

@@ -32,8 +32,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { isEqual, diff } from 'ohash' import { isEqual } from 'ohash'
import { type PropType, defineComponent } from 'vue' import { defineComponent } from 'vue'
import { nuxtLinkProps } from '../../utils' import { nuxtLinkProps } from '../../utils'
export default defineComponent({ export default defineComponent({
@@ -61,7 +61,7 @@ export default defineComponent({
default: false default: false
}, },
exactQuery: { exactQuery: {
type: [Boolean, String] as PropType<boolean | 'partial'>, type: Boolean,
default: false default: false
}, },
exactHash: { exactHash: {
@@ -74,21 +74,9 @@ export default defineComponent({
} }
}, },
setup(props) { setup(props) {
function isPartiallyEqual(item1, item2) {
const diffedKeys = diff(item1, item2).reduce((filtered, q) => {
if (q.type === 'added') {
filtered.push(q.key)
}
return filtered
}, [])
return isEqual(item1, item2, { excludeKeys: key => diffedKeys.includes(key) })
}
function resolveLinkClass(route, $route, { isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) { function resolveLinkClass(route, $route, { isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
if (props.exactQuery === 'partial') { if (props.exactQuery && !isEqual(route.query, $route.query)) {
if (!isPartiallyEqual(route.query, $route.query)) return props.inactiveClass return props.inactiveClass
} else if (props.exactQuery === true) {
if (!isEqual(route.query, $route.query)) return props.inactiveClass
} }
if (props.exactHash && route.hash !== $route.hash) { if (props.exactHash && route.hash !== $route.hash) {
return props.inactiveClass return props.inactiveClass

View File

@@ -60,8 +60,6 @@ export default defineComponent({
const formId = useId() const formId = useId()
const bus = useEventBus<FormEvent>(`form-${formId}`) const bus = useEventBus<FormEvent>(`form-${formId}`)
const parsedValue = ref(null)
onMounted(() => { onMounted(() => {
bus.on(async (event) => { bus.on(async (event) => {
if (event.type !== 'submit' && props.validateOn?.includes(event.type)) { if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {
@@ -89,7 +87,7 @@ export default defineComponent({
if (errors) { if (errors) {
errs = errs.concat(errors) errs = errs.concat(errors)
} else { } else {
parsedValue.value = result Object.assign(props.state, result)
} }
} }
@@ -132,7 +130,7 @@ export default defineComponent({
if (props.validateOn?.includes('submit')) { if (props.validateOn?.includes('submit')) {
await validate() await validate()
} }
event.data = props.schema ? parsedValue.value : props.state event.data = props.state
emit('submit', event) emit('submit', event)
} catch (error) { } catch (error) {
if (!(error instanceof FormException)) { if (!(error instanceof FormException)) {
@@ -323,7 +321,7 @@ async function validateYupSchema(
schema: YupObjectSchema<any> schema: YupObjectSchema<any>
): Promise<ValidateReturnSchema<typeof state>> { ): Promise<ValidateReturnSchema<typeof state>> {
try { try {
const result = await schema.validate(state, { abortEarly: false }) const result = schema.validateSync(state, { abortEarly: false })
return { return {
errors: null, errors: null,
result result

View File

@@ -293,24 +293,6 @@ export default defineComponent({
const size = computed(() => sizeButtonGroup.value ?? sizeFormGroup.value) const size = computed(() => sizeButtonGroup.value ?? sizeFormGroup.value)
const by = computed(() => {
if (!props.by) return undefined
if (typeof props.by === 'function') {
return props.by
}
const key = props.by
const hasDot = key.indexOf('.')
if (hasDot > 0) {
return (a: any, z: any) => {
return accessor(a, key) === accessor(z, key)
}
}
return key
})
const internalQuery = ref('') const internalQuery = ref('')
const query = computed({ const query = computed({
get() { get() {
@@ -323,7 +305,9 @@ export default defineComponent({
}) })
const label = computed(() => { const label = computed(() => {
if (!props.modelValue) return null if (!props.modelValue) {
return
}
function getValue(value: any) { function getValue(value: any) {
if (props.valueAttribute) { if (props.valueAttribute) {
@@ -334,7 +318,7 @@ export default defineComponent({
} }
function compareValues(value1: any, value2: any) { function compareValues(value1: any, value2: any) {
if (by.value && typeof by.value !== 'function' && typeof value1 === 'object' && typeof value2 === 'object') { if (props.by && typeof value1 === 'object' && typeof value2 === 'object') {
return isEqual(value1[props.by], value2[props.by]) return isEqual(value1[props.by], value2[props.by])
} }
return isEqual(value1, value2) return isEqual(value1, value2)
@@ -523,9 +507,7 @@ export default defineComponent({
query, query,
accessor, accessor,
onUpdate, onUpdate,
onQueryChange, onQueryChange
// eslint-disable-next-line vue/no-dupe-keys
by
} }
} }
}) })

View File

@@ -39,6 +39,18 @@
<span v-if="label" :class="uiMenu.label">{{ label }}</span> <span v-if="label" :class="uiMenu.label">{{ label }}</span>
<span v-else :class="uiMenu.label">{{ placeholder || '&nbsp;' }}</span> <span v-else :class="uiMenu.label">{{ placeholder || '&nbsp;' }}</span>
</slot> </slot>
<span v-if="canClearValue" :class="clearableWrapperClass">
<slot name="clearable" :selected="selected" :disabled="disabled" :loading="loading" @clear="onClear">
<UButton
:icon="clearableIcon"
size="xs"
class="p-0"
:class="clearableButtonClass"
variant="ghost"
@click.capture.stop="onClear"
/>
</slot>
</span>
<span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass"> <span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
<slot name="trailing" :selected="selected" :disabled="disabled" :loading="loading"> <slot name="trailing" :selected="selected" :disabled="disabled" :loading="loading">
@@ -149,6 +161,7 @@ import { useFormGroup } from '../../composables/useFormGroup'
import { get, mergeConfig } from '../../utils' import { get, mergeConfig } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup' import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { SelectSize, SelectColor, SelectVariant, PopperOptions, Strategy, DeepPartial } from '../../types/index' import type { SelectSize, SelectColor, SelectVariant, PopperOptions, Strategy, DeepPartial } from '../../types/index'
import type { Button } from '../../types/button'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' import appConfig from '#build/app.config'
import { select, selectMenu } from '#ui/ui.config' import { select, selectMenu } from '#ui/ui.config'
@@ -333,9 +346,18 @@ export default defineComponent({
uiMenu: { uiMenu: {
type: Object as PropType<DeepPartial<typeof configMenu> & { strategy?: Strategy }>, type: Object as PropType<DeepPartial<typeof configMenu> & { strategy?: Strategy }>,
default: () => ({}) default: () => ({})
},
clearable: {
type: Boolean,
default: false
},
clearableIcon: {
type: String,
default: () => config.default.clerableIcon
} }
}, },
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change'], emits: ['update:modelValue', 'update:query', 'open', 'close', 'change', 'clear'],
setup(props, { emit, slots }) { setup(props, { emit, slots }) {
if (import.meta.dev && props.multiple && !Array.isArray(props.modelValue)) { if (import.meta.dev && props.multiple && !Array.isArray(props.modelValue)) {
console.warn(`[@nuxt/ui] The USelectMenu components needs to have a modelValue of type Array when using the multiple prop. Got '${typeof props.modelValue}' instead.`, props.modelValue) console.warn(`[@nuxt/ui] The USelectMenu components needs to have a modelValue of type Array when using the multiple prop. Got '${typeof props.modelValue}' instead.`, props.modelValue)
@@ -348,24 +370,6 @@ export default defineComponent({
const [trigger, container] = usePopper(popper.value) const [trigger, container] = usePopper(popper.value)
const by = computed(() => {
if (!props.by) return undefined
if (typeof props.by === 'function') {
return props.by
}
const key = props.by
const hasDot = key.indexOf('.')
if (hasDot > 0) {
return (a: any, z: any) => {
return accessor(a, key) === accessor(z, key)
}
}
return key
})
const { size: sizeButtonGroup, rounded } = useInjectButtonGroup({ ui, props }) const { size: sizeButtonGroup, rounded } = useInjectButtonGroup({ ui, props })
const { emitFormBlur, emitFormChange, inputId, color, size: sizeFormGroup, name } = useFormGroup(props, config) const { emitFormBlur, emitFormChange, inputId, color, size: sizeFormGroup, name } = useFormGroup(props, config)
@@ -384,8 +388,8 @@ export default defineComponent({
const selected = computed(() => { const selected = computed(() => {
function compareValues(value1: any, value2: any) { function compareValues(value1: any, value2: any) {
if (by.value && typeof by.value !== 'function' && typeof value1 === 'object' && typeof value2 === 'object') { if (props.by && typeof value1 === 'object' && typeof value2 === 'object') {
return isEqual(value1[by.value], value2[by.value]) return isEqual(value1[props.by], value2[props.by])
} }
return isEqual(value1, value2) return isEqual(value1, value2)
} }
@@ -417,12 +421,16 @@ export default defineComponent({
}) })
const label = computed(() => { const label = computed(() => {
if (!props.modelValue) return null if (!selected.value) return null
if (props.valueAttribute) {
return accessor(selected.value as Record<string, any>, props.optionAttribute)
}
if (Array.isArray(props.modelValue) && props.modelValue.length) { if (Array.isArray(props.modelValue) && props.modelValue.length) {
return `${props.modelValue.length} selected` return `${props.modelValue.length} selected`
} else if (['string', 'number'].includes(typeof props.modelValue)) { } else if (['string', 'number'].includes(typeof props.modelValue)) {
return props.valueAttribute ? accessor(selected.value, props.optionAttribute) : props.modelValue return props.modelValue
} }
return accessor(props.modelValue as Record<string, any>, props.optionAttribute) return accessor(props.modelValue as Record<string, any>, props.optionAttribute)
@@ -460,6 +468,23 @@ export default defineComponent({
return props.leadingIcon || props.icon return props.leadingIcon || props.icon
}) })
const canClearValue = computed(() => props.clearable && (Array.isArray(selected.value) ? selected.value.length > 0 : !!selected.value))
const clearableWrapperClass = computed(() => {
return twJoin(
ui.value.icon.clearable.wrapper,
ui.value.icon.clearable.padding[size.value]
)
})
const clearableButtonClass = computed(() => {
return twJoin(
ui.value.icon.base,
color.value && appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
props.loading && ui.value.icon.loading
)
})
const trailingIconName = computed(() => { const trailingIconName = computed(() => {
if (props.loading && !isLeading.value) { if (props.loading && !isLeading.value) {
return props.loadingIcon return props.loadingIcon
@@ -488,7 +513,6 @@ export default defineComponent({
const trailingWrapperIconClass = computed(() => { const trailingWrapperIconClass = computed(() => {
return twJoin( return twJoin(
ui.value.icon.trailing.wrapper, ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.pointer,
ui.value.icon.trailing.padding[size.value] ui.value.icon.trailing.padding[size.value]
) )
}) })
@@ -563,7 +587,7 @@ export default defineComponent({
return ['string', 'number'].includes(typeof props.modelValue) ? query.value : { [props.optionAttribute]: query.value } return ['string', 'number'].includes(typeof props.modelValue) ? query.value : { [props.optionAttribute]: query.value }
}) })
function clearOnClose() { function handleClearSearchOnClose() {
if (props.clearSearchOnClose) { if (props.clearSearchOnClose) {
query.value = '' query.value = ''
} }
@@ -573,7 +597,7 @@ export default defineComponent({
if (value) { if (value) {
emit('open') emit('open')
} else { } else {
clearOnClose() handleClearSearchOnClose()
emit('close') emit('close')
emitFormBlur() emitFormBlur()
} }
@@ -593,6 +617,28 @@ export default defineComponent({
query.value = event.target.value query.value = event.target.value
} }
function onClear() {
if (canClearValue.value) {
emit('update:modelValue', props.multiple ? [] : null)
emit('clear')
emitFormChange()
}
}
function trailingSlotProps() {
const slotProps: Record<string, any> = {
selected: selected.value,
loading: props.loading,
disabled: props.disabled
}
if (props.clearable) {
slotProps.onClear = onClear
}
return slotProps
}
provideUseId(() => useId()) provideUseId(() => useId())
return { return {
@@ -612,6 +658,7 @@ export default defineComponent({
label, label,
accessor, accessor,
isLeading, isLeading,
onClear,
isTrailing, isTrailing,
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
selectClass, selectClass,
@@ -627,8 +674,10 @@ export default defineComponent({
query, query,
onUpdate, onUpdate,
onQueryChange, onQueryChange,
// eslint-disable-next-line vue/no-dupe-keys trailingSlotProps,
by canClearValue,
clearableWrapperClass,
clearableButtonClass
} }
} }
}) })

View File

@@ -117,10 +117,6 @@ export default defineComponent({
ui: { ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>, type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}) default: () => ({})
},
pauseTimeoutOnHover: {
type: Boolean,
default: true
} }
}, },
emits: ['close'], emits: ['close'],
@@ -161,13 +157,13 @@ export default defineComponent({
}) })
function onMouseover() { function onMouseover() {
if (props.pauseTimeoutOnHover && timer) { if (timer) {
timer.pause() timer.pause()
} }
} }
function onMouseleave() { function onMouseleave() {
if (props.pauseTimeoutOnHover && timer) { if (timer) {
timer.resume() timer.resume()
} }
} }

View File

@@ -1,3 +0,0 @@
import type colors from '#ui-colors'
export type CheckboxColor = typeof colors[number]

View File

@@ -1,3 +1,3 @@
import type { divider } from '../ui.config' import type { divider } from '#ui/ui.config'
export type DividerSize = keyof typeof divider.border.size.horizontal | keyof typeof divider.border.size.vertical export type DividerSize = keyof typeof divider.border.size.horizontal | keyof typeof divider.border.size.vertical

View File

@@ -4,7 +4,6 @@ export * from './avatar'
export * from './badge' export * from './badge'
export * from './breadcrumb' export * from './breadcrumb'
export * from './button' export * from './button'
export * from './checkbox'
export * from './chip' export * from './chip'
export * from './clipboard' export * from './clipboard'
export * from './command-palette' export * from './command-palette'

View File

@@ -6,7 +6,7 @@ export interface Link extends NuxtLinkProps {
disabled?: boolean disabled?: boolean
active?: boolean active?: boolean
exact?: boolean exact?: boolean
exactQuery?: boolean | 'partial' exactQuery?: boolean
exactHash?: boolean exactHash?: boolean
inactiveClass?: string inactiveClass?: string
} }

View File

@@ -7,7 +7,7 @@ export interface TightMap<O = any> {
export type DeepPartial<T, O = any> = { export type DeepPartial<T, O = any> = {
[P in keyof T]?: T[P] extends object [P in keyof T]?: T[P] extends object
? DeepPartial<T[P], O> ? DeepPartial<T[P], O>
: T[P] extends string ? string : T[P]; : T[P];
} & { } & {
[key: string]: O | TightMap<O> [key: string]: O | TightMap<O>
} }

View File

@@ -1,5 +1,3 @@
import type { ButtonColor, ButtonSize, ButtonVariant, CheckboxColor, ProgressAnimation, ProgressColor } from '../../types'
export default { export default {
wrapper: 'relative overflow-x-auto', wrapper: 'relative overflow-x-auto',
base: 'min-w-full table-fixed', base: 'min-w-full table-fixed',
@@ -53,23 +51,23 @@ export default {
icon: 'i-heroicons-arrows-up-down-20-solid', icon: 'i-heroicons-arrows-up-down-20-solid',
trailing: true, trailing: true,
square: true, square: true,
color: 'gray' as ButtonColor, color: 'gray' as const,
variant: 'ghost' as ButtonVariant, variant: 'ghost' as const,
class: '-m-1.5' class: '-m-1.5'
}, },
expandButton: { expandButton: {
icon: 'i-heroicons-chevron-down', icon: 'i-heroicons-chevron-down',
color: 'gray' as ButtonColor, color: 'gray' as const,
variant: 'ghost' as ButtonVariant, variant: 'ghost' as const,
size: 'xs' as ButtonSize, size: 'xs' as const,
class: '-my-1.5 align-middle' class: '-my-1.5 align-middle'
}, },
checkbox: { checkbox: {
color: 'primary' as CheckboxColor color: 'primary' as const
}, },
progress: { progress: {
color: 'primary' as ProgressColor, color: 'primary' as const,
animation: 'carousel' as ProgressAnimation animation: 'carousel' as const
}, },
loadingState: { loadingState: {
icon: 'i-heroicons-arrow-path-20-solid', icon: 'i-heroicons-arrow-path-20-solid',

View File

@@ -16,7 +16,7 @@ export default {
openIcon: 'i-heroicons-chevron-down-20-solid', openIcon: 'i-heroicons-chevron-down-20-solid',
closeIcon: '', closeIcon: '',
class: 'mb-1.5 w-full', class: 'mb-1.5 w-full',
variant: 'soft', variant: 'soft' as const,
truncate: true truncate: true
} }
} }

View File

@@ -1,5 +1,3 @@
import type { AvatarSize, ButtonColor, ButtonSize, ButtonVariant } from '../../types'
export default { export default {
wrapper: 'w-full relative overflow-hidden', wrapper: 'w-full relative overflow-hidden',
inner: 'w-0 flex-1', inner: 'w-0 flex-1',
@@ -15,7 +13,7 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0 self-center', base: 'flex-shrink-0 self-center',
size: 'md' as AvatarSize size: 'md' as const
}, },
color: { color: {
white: { white: {
@@ -34,9 +32,9 @@ export default {
icon: null, icon: null,
closeButton: null, closeButton: null,
actionButton: { actionButton: {
size: 'xs' as ButtonSize, size: 'xs' as const,
color: 'primary' as ButtonColor, color: 'primary' as const,
variant: 'link' as ButtonVariant variant: 'link' as const
} }
} }
} }

View File

@@ -8,12 +8,6 @@ export default {
md: 'text-sm px-2 py-1', md: 'text-sm px-2 py-1',
lg: 'text-sm px-2.5 py-1.5' lg: 'text-sm px-2.5 py-1.5'
}, },
gap: {
xs: 'gap-0.5',
sm: 'gap-1',
md: 'gap-1',
lg: 'gap-1.5'
},
color: { color: {
white: { white: {
solid: 'ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-900 dark:text-white bg-white dark:bg-gray-900' solid: 'ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-900 dark:text-white bg-white dark:bg-gray-900'
@@ -31,15 +25,6 @@ export default {
soft: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400', soft: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400',
subtle: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-25 dark:ring-opacity-25' subtle: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-25 dark:ring-opacity-25'
}, },
icon: {
base: 'flex-shrink-0',
size: {
xs: 'h-4 w-4',
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-5 w-5'
}
},
default: { default: {
size: 'sm', size: 'sm',
variant: 'solid', variant: 'solid',

View File

@@ -1,5 +1,3 @@
import type { ButtonColor } from '../../types'
export default { export default {
wrapper: 'relative', wrapper: 'relative',
container: 'relative w-full flex overflow-x-auto snap-x snap-mandatory scroll-smooth', container: 'relative w-full flex overflow-x-auto snap-x snap-mandatory scroll-smooth',
@@ -15,12 +13,12 @@ export default {
}, },
default: { default: {
prevButton: { prevButton: {
color: 'black' as ButtonColor, color: 'black' as const,
class: 'rtl:[&_span:first-child]:rotate-180 absolute start-4 top-1/2 transform -translate-y-1/2 rounded-full', class: 'rtl:[&_span:first-child]:rotate-180 absolute start-4 top-1/2 transform -translate-y-1/2 rounded-full',
icon: 'i-heroicons-chevron-left-20-solid' icon: 'i-heroicons-chevron-left-20-solid'
}, },
nextButton: { nextButton: {
color: 'black' as ButtonColor, color: 'black' as const,
class: 'rtl:[&_span:last-child]:rotate-180 absolute end-4 top-1/2 transform -translate-y-1/2 rounded-full', class: 'rtl:[&_span:last-child]:rotate-180 absolute end-4 top-1/2 transform -translate-y-1/2 rounded-full',
icon: 'i-heroicons-chevron-right-20-solid' icon: 'i-heroicons-chevron-right-20-solid'
} }

View File

@@ -1,4 +1,3 @@
import type { AvatarSize } from '../../types'
import { arrow } from '../popper' import { arrow } from '../popper'
export default { export default {
@@ -29,7 +28,7 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as AvatarSize size: '2xs' as const
}, },
label: 'truncate', label: 'truncate',
shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5 ms-auto' shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5 ms-auto'

View File

@@ -98,6 +98,18 @@ export default {
'lg': 'px-3.5', 'lg': 'px-3.5',
'xl': 'px-3.5' 'xl': 'px-3.5'
} }
},
clearable: {
wrapper: 'absolute inset-y-0 end-6 flex items-center',
pointer: 'pointer-events-auto',
padding: {
'2xs': 'px-2',
'xs': 'px-2.5',
'sm': 'px-2.5',
'md': 'px-3',
'lg': 'px-3.5',
'xl': 'px-3.5'
}
} }
}, },
default: { default: {

View File

@@ -1,4 +1,3 @@
import type { AvatarSize } from '../../types'
import { arrow } from '../popper' import { arrow } from '../popper'
export default { export default {
@@ -37,7 +36,7 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as AvatarSize size: '2xs' as const
}, },
chip: { chip: {
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full' base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'

View File

@@ -9,6 +9,7 @@ export default {
color: 'white', color: 'white',
variant: 'outline', variant: 'outline',
loadingIcon: 'i-heroicons-arrow-path-20-solid', loadingIcon: 'i-heroicons-arrow-path-20-solid',
trailingIcon: 'i-heroicons-chevron-down-20-solid' trailingIcon: 'i-heroicons-chevron-down-20-solid',
clerableIcon: 'i-heroicons-x-mark-20-solid'
} }
} }

View File

@@ -1,5 +1,3 @@
import type { AvatarSize } from '../../types'
export default { export default {
wrapper: { wrapper: {
base: 'flex items-center align-center text-center', base: 'flex items-center align-center text-center',
@@ -44,11 +42,11 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as AvatarSize size: '2xs' as const
}, },
label: 'text-sm', label: 'text-sm',
default: { default: {
size: '2xs', size: '2xs' as const,
type: 'solid' type: 'solid' as const
} }
} }

View File

@@ -1,5 +1,3 @@
import type { AvatarSize } from '../../types'
export default { export default {
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800', wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800',
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2', container: 'relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2',
@@ -48,7 +46,7 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as AvatarSize size: '2xs' as const
}, },
chip: { chip: {
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full' base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'

View File

@@ -1,5 +1,3 @@
import type { AvatarSize, BadgeColor, BadgeSize, BadgeVariant } from '../../types'
export default { export default {
wrapper: 'relative w-full flex items-center justify-between', wrapper: 'relative w-full flex items-center justify-between',
container: 'flex items-center min-w-0', container: 'flex items-center min-w-0',
@@ -17,12 +15,12 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as AvatarSize size: '2xs' as const
}, },
badge: { badge: {
base: 'flex-shrink-0 ms-auto relative rounded', base: 'flex-shrink-0 ms-auto relative rounded',
color: 'gray' as BadgeColor, color: 'gray' as const,
variant: 'solid' as BadgeVariant, variant: 'solid' as const,
size: 'xs' as BadgeSize size: 'xs' as const
} }
} }

View File

@@ -1,5 +1,3 @@
import type { ButtonColor } from '../../types'
export default { export default {
wrapper: 'flex items-center -space-x-px', wrapper: 'flex items-center -space-x-px',
base: '', base: '',
@@ -7,28 +5,28 @@ export default {
default: { default: {
size: 'sm', size: 'sm',
activeButton: { activeButton: {
color: 'primary' as ButtonColor color: 'primary' as const
}, },
inactiveButton: { inactiveButton: {
color: 'white' as ButtonColor color: 'white' as const
}, },
firstButton: { firstButton: {
color: 'white' as ButtonColor, color: 'white' as const,
class: 'rtl:[&_span:first-child]:rotate-180', class: 'rtl:[&_span:first-child]:rotate-180',
icon: 'i-heroicons-chevron-double-left-20-solid' icon: 'i-heroicons-chevron-double-left-20-solid'
}, },
lastButton: { lastButton: {
color: 'white' as ButtonColor, color: 'white' as const,
class: 'rtl:[&_span:last-child]:rotate-180', class: 'rtl:[&_span:last-child]:rotate-180',
icon: 'i-heroicons-chevron-double-right-20-solid' icon: 'i-heroicons-chevron-double-right-20-solid'
}, },
prevButton: { prevButton: {
color: 'white' as ButtonColor, color: 'white' as const,
class: 'rtl:[&_span:first-child]:rotate-180', class: 'rtl:[&_span:first-child]:rotate-180',
icon: 'i-heroicons-chevron-left-20-solid' icon: 'i-heroicons-chevron-left-20-solid'
}, },
nextButton: { nextButton: {
color: 'white' as ButtonColor, color: 'white' as const,
class: 'rtl:[&_span:last-child]:rotate-180', class: 'rtl:[&_span:last-child]:rotate-180',
icon: 'i-heroicons-chevron-right-20-solid' icon: 'i-heroicons-chevron-right-20-solid'
} }

View File

@@ -1,5 +1,3 @@
import type { AvatarSize, BadgeColor, BadgeSize, BadgeVariant } from '../../types'
export default { export default {
wrapper: 'relative', wrapper: 'relative',
base: 'group relative flex items-center gap-1.5 focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75', base: 'group relative flex items-center gap-1.5 focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75',
@@ -19,13 +17,13 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as AvatarSize size: '2xs' as const
}, },
badge: { badge: {
base: 'flex-shrink-0 ms-auto relative rounded', base: 'flex-shrink-0 ms-auto relative rounded',
color: 'gray' as BadgeColor, color: 'gray' as const,
variant: 'solid' as BadgeVariant, variant: 'solid' as const,
size: 'xs' as BadgeSize size: 'xs' as const
}, },
divider: { divider: {
wrapper: { wrapper: {

View File

@@ -1,5 +1,3 @@
import type { AvatarSize, ButtonColor, ButtonSize, ButtonVariant } from '../../types'
export default { export default {
wrapper: 'w-full pointer-events-auto', wrapper: 'w-full pointer-events-auto',
container: 'relative overflow-hidden', container: 'relative overflow-hidden',
@@ -19,7 +17,7 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0 self-center', base: 'flex-shrink-0 self-center',
size: 'md' as AvatarSize size: 'md' as const
}, },
progress: { progress: {
base: 'absolute bottom-0 end-0 start-0 h-1', base: 'absolute bottom-0 end-0 start-0 h-1',
@@ -40,13 +38,13 @@ export default {
timeout: 5000, timeout: 5000,
closeButton: { closeButton: {
icon: 'i-heroicons-x-mark-20-solid', icon: 'i-heroicons-x-mark-20-solid',
color: 'gray' as ButtonColor, color: 'gray' as const,
variant: 'link' as ButtonVariant, variant: 'link' as const,
padded: false padded: false
}, },
actionButton: { actionButton: {
size: 'xs' as ButtonSize, size: 'xs' as const,
color: 'white' as ButtonColor color: 'white' as const
} }
} }
} }