mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 20:48:12 +01:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c368c8ab8 | ||
|
|
c5796c4f82 | ||
|
|
204953b780 | ||
|
|
2e4c3082a1 | ||
|
|
f2fd778c0a | ||
|
|
d79da9d7b6 | ||
|
|
a4429eee09 | ||
|
|
0905b2b3d5 | ||
|
|
c7fba2e0eb | ||
|
|
4167f04205 | ||
|
|
276268d311 | ||
|
|
717e35f098 | ||
|
|
459a0410ab | ||
|
|
b9adc83e78 | ||
|
|
d7a4d029b7 | ||
|
|
3c8d6cd01d | ||
|
|
67da90a2f6 | ||
|
|
894e8a61b6 | ||
|
|
1b6ab271ea | ||
|
|
0dc4678c68 | ||
|
|
e86dc79e51 | ||
|
|
35997377a6 | ||
|
|
12303a87be | ||
|
|
f84ccddcd6 | ||
|
|
869c0708bd | ||
|
|
c63d2f380a | ||
|
|
92632e969e | ||
|
|
f6d7994a55 | ||
|
|
f738f68f76 | ||
|
|
17d6803329 | ||
|
|
732a67aa88 | ||
|
|
bdf129fc38 | ||
|
|
d140acc608 | ||
|
|
cc20a26f07 | ||
|
|
983c6382d1 | ||
|
|
37eabc89bd | ||
|
|
a57844e416 | ||
|
|
2be60cddfe | ||
|
|
09b4699aea | ||
|
|
46c2987ebf | ||
|
|
f244d15b96 | ||
|
|
aaa60c0798 | ||
|
|
5467d71cc2 | ||
|
|
941a54e5e3 | ||
|
|
655f98ffed | ||
|
|
999a0f8467 | ||
|
|
2739939c46 | ||
|
|
2a241c87c3 | ||
|
|
e6e510b848 | ||
|
|
a655da1394 | ||
|
|
3a71256d59 | ||
|
|
404359a6ca | ||
|
|
1e4e9c4708 | ||
|
|
6f07f6bd6e | ||
|
|
4c1093bde4 | ||
|
|
7d51a9e479 | ||
|
|
04bdbcfc6e | ||
|
|
58aa296425 | ||
|
|
d3df3bb929 | ||
|
|
4863775e17 | ||
|
|
7551a85ad2 | ||
|
|
c2bcb8e264 | ||
|
|
88124b85c5 | ||
|
|
9f539c9545 | ||
|
|
41d4ffe5b5 | ||
|
|
d8a0bbe710 | ||
|
|
06e5689da8 | ||
|
|
67d19f582a | ||
|
|
650bc40253 | ||
|
|
2f0f317a12 | ||
|
|
d2a26939ad | ||
|
|
b7a8146898 | ||
|
|
e81464a43e | ||
|
|
4b3d2a7b00 |
4
.github/ISSUE_TEMPLATE/bug-report-v3.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report-v3.yml
vendored
@@ -5,7 +5,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before reporting a bug, please make sure that you have read through our [v3 documentation](https://ui.nuxt.com/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
|
||||
Before reporting a bug, please make sure that you have read through our [documentation](https://ui.nuxt.com/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
|
||||
- type: textarea
|
||||
id: env
|
||||
attributes:
|
||||
@@ -44,7 +44,7 @@ body:
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction
|
||||
description: Please provide a reproduction link using the Nuxt template https://codesandbox.io/p/devbox/nuxt-ui3-n3sxks or the Vue template https://codesandbox.io/p/devbox/nuxt-ui3-vue-4h5gqn. A minimal [reproduction is required](https://antfu.me/posts/why-reproductions-are-required) unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "needs reproduction" label. If no reproduction is provided we might close it.
|
||||
description: Please provide a reproduction link using the Nuxt template https://codesandbox.io/p/devbox/nuxt-ui3-n3sxks or the Vue template https://codesandbox.io/p/devbox/nuxt-ui3-vue-4h5gqn. A minimal [reproduction is required](https://antfu.me/posts/why-reproductions-are-required) unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "needs reproduction" label. If no reproduction is provided, it will be closed automatically after a while.
|
||||
placeholder: https://github.com/my/reproduction
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -5,7 +5,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before requesting a feature, please make sure that you have read through our [v3 documentation](https://ui.nuxt.com/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
|
||||
Before requesting a feature, please make sure that you have read through our [documentation](https://ui.nuxt.com/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/question-v3.yml
vendored
2
.github/ISSUE_TEMPLATE/question-v3.yml
vendored
@@ -5,7 +5,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before asking a question, please make sure that you have read through our [v3 documentation](https://ui.nuxt.com/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
|
||||
Before asking a question, please make sure that you have read through our [documentation](https://ui.nuxt.com/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
|
||||
30
.github/reproduire/needs-reproduction.md
vendored
Normal file
30
.github/reproduire/needs-reproduction.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
Would you be able to provide a [reproduction](https://nuxt.com/docs/community/reporting-bugs/#create-a-minimal-reproduction)? 🙏
|
||||
|
||||
<details>
|
||||
<summary>More info</summary>
|
||||
|
||||
### Why do I need to provide a reproduction?
|
||||
|
||||
Reproductions make it possible for us to triage and fix issues quickly with a relatively small team. It helps us discover the source of the problem, and also can reveal assumptions you or we might be making.
|
||||
|
||||
### What will happen?
|
||||
|
||||
If you've provided a reproduction, we'll remove the label and try to reproduce the issue. If we can, we'll mark it as a bug and prioritise it based on its severity and how many people we think it might affect.
|
||||
|
||||
If `needs reproduction` labeled issues don't receive any substantial activity (e.g., new comments featuring a reproduction link), they will be closed automatically after a while. That's not because we don't care! At any point, feel free to comment with a reproduction and we'll reopen it.
|
||||
|
||||
### How can I create a reproduction?
|
||||
|
||||
We have templates to create a minimal reproduction:
|
||||
|
||||
* **Nuxt**: https://codesandbox.io/p/devbox/nuxt-ui3-n3sxks
|
||||
* **Vue**: https://codesandbox.io/p/devbox/nuxt-ui3-vue-4h5gqn
|
||||
|
||||
Please ensure that the reproduction is as **minimal** as possible. See more details [in our guide](https://nuxt.com/docs/community/reporting-bugs/#create-a-minimal-reproduction).
|
||||
|
||||
You might also find these other articles interesting and/or helpful:
|
||||
|
||||
- [The Importance of Reproductions](https://antfu.me/posts/why-reproductions-are-required)
|
||||
- [How to Generate a Minimal, Complete, and Verifiable Example](https://stackoverflow.com/help/mcve)
|
||||
|
||||
</details>
|
||||
17
.github/workflows/reproduire.yml
vendored
Normal file
17
.github/workflows/reproduire.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: reproduire
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
reproduire:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp
|
||||
with:
|
||||
label: needs reproduction
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,5 +1,46 @@
|
||||
# Changelog
|
||||
|
||||
## [3.1.2](https://github.com/nuxt/ui/compare/v3.1.1...v3.1.2) (2025-05-15)
|
||||
|
||||
### Features
|
||||
|
||||
* **Badge:** add `square` prop ([#4008](https://github.com/nuxt/ui/issues/4008)) ([894e8a6](https://github.com/nuxt/ui/commit/894e8a61b6fea3618fc863bd77678385e9d021c2))
|
||||
* **CheckboxGroup:** add `table` variant ([#3997](https://github.com/nuxt/ui/issues/3997)) ([1b6ab27](https://github.com/nuxt/ui/commit/1b6ab271ea3875a7c77ffe9367c7c341083dd53c))
|
||||
* **components:** add `ui` field in items ([#4060](https://github.com/nuxt/ui/issues/4060)) ([b9adc83](https://github.com/nuxt/ui/commit/b9adc83e787db02507e6e7bb1aabc684eccc197b))
|
||||
* **InputNumber:** add `increment-disabled` / `decrement-disabled` props ([#4141](https://github.com/nuxt/ui/issues/4141)) ([c7fba2e](https://github.com/nuxt/ui/commit/c7fba2e0ebfb7153f3bfb727165d653bbd3dbe54))
|
||||
* **locale:** add Slovenian language ([#4140](https://github.com/nuxt/ui/issues/4140)) ([e86dc79](https://github.com/nuxt/ui/commit/e86dc79e51b2773a77ada5f12d4f0964fbc83354))
|
||||
* **NavigationMenu:** add `collapsible` field in items ([2be60cd](https://github.com/nuxt/ui/commit/2be60cddfe10fd1e2466900fd53e21ee0c877227)), closes [#3353](https://github.com/nuxt/ui/issues/3353) [#3911](https://github.com/nuxt/ui/issues/3911)
|
||||
* **NavigationMenu:** handle `tooltip` in items ([46c2987](https://github.com/nuxt/ui/commit/46c2987ebfd30b2b071a96a745b7270e852e96de)), closes [#4050](https://github.com/nuxt/ui/issues/4050)
|
||||
* **Slider:** handle `tooltip` around thumbs ([d140acc](https://github.com/nuxt/ui/commit/d140acc608c6ae11c0a0531fe443588776ea7807)), closes [#1469](https://github.com/nuxt/ui/issues/1469)
|
||||
* **Toast:** add `progress` prop to hide progress bar ([#4125](https://github.com/nuxt/ui/issues/4125)) ([92632e9](https://github.com/nuxt/ui/commit/92632e969eaa11521a166e50e346753929b7f523))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Badge/Button:** handle zero value in label correctly ([#4108](https://github.com/nuxt/ui/issues/4108)) ([f244d15](https://github.com/nuxt/ui/commit/f244d15b96d97cd8ba34ba9c18f23965e17e3cef))
|
||||
* **ButtonGroup:** add `z-index` on focused element ([204953b](https://github.com/nuxt/ui/commit/204953b780bde08dbfde230fc8887674449227b7))
|
||||
* **Calendar:** wrong color for today date with `neutral` color ([7d51a9e](https://github.com/nuxt/ui/commit/7d51a9e479cb6105ea37759c5cd67ff9f7702c49)), closes [#4084](https://github.com/nuxt/ui/issues/4084) [#3629](https://github.com/nuxt/ui/issues/3629)
|
||||
* **Checkbox/RadioGroup:** render correct element without `variant` ([f2fd778](https://github.com/nuxt/ui/commit/f2fd778c0a604f2d65aec9f3fe2d54b6d4e8c3a2)), closes [#3998](https://github.com/nuxt/ui/issues/3998)
|
||||
* **CheckboxGroup:** relative `UCheckbox` import ([7551a85](https://github.com/nuxt/ui/commit/7551a85ad2d92b59e2909396affb862403d5b27a)), closes [#4090](https://github.com/nuxt/ui/issues/4090)
|
||||
* **ColorPicker:** make thumb touch draggable ([#4101](https://github.com/nuxt/ui/issues/4101)) ([cc20a26](https://github.com/nuxt/ui/commit/cc20a26f07268d19119ab4c7c254033143bb63f4))
|
||||
* **components:** `class` should have priority over `ui` prop ([e6e510b](https://github.com/nuxt/ui/commit/e6e510b848d995a286a51d50a120d67483e11232))
|
||||
* **FormField:** block form field injection after use ([#4150](https://github.com/nuxt/ui/issues/4150)) ([d79da9d](https://github.com/nuxt/ui/commit/d79da9d7b60c9972af64acd8e6eef4ae7d6bc3eb))
|
||||
* **FormField:** use `div` for `error` and `help` slots ([459a041](https://github.com/nuxt/ui/commit/459a0410ab729fde60865e84632b36903465f57e))
|
||||
* **inertia:** link always render as anchor tag ([#3989](https://github.com/nuxt/ui/issues/3989)) ([e81464a](https://github.com/nuxt/ui/commit/e81464a43ede4e63ce3dc92429bbfef48614f731))
|
||||
* **inertia:** make `useAppConfig` reactive ([12303a8](https://github.com/nuxt/ui/commit/12303a87be62dae84ef774e3a9795deb0ac90cc7))
|
||||
* **Input/Textarea:** handle generic types ([3c8d6cd](https://github.com/nuxt/ui/commit/3c8d6cd01dfafed5844c376f52adbdda0c814420)), closes [nuxt/ui-pro#887](https://github.com/nuxt/ui-pro/issues/887)
|
||||
* **InputNumber:** handle inside button group ([2e4c308](https://github.com/nuxt/ui/commit/2e4c3082a1e66fa597086dc3431fec37fa29ef62)), closes [#4155](https://github.com/nuxt/ui/issues/4155)
|
||||
* **Link:** consistent behavior between nuxt, vue and inertia ([#4134](https://github.com/nuxt/ui/issues/4134)) ([67da90a](https://github.com/nuxt/ui/commit/67da90a2f638124f640c4271d3376c5ff3fab6a1))
|
||||
* **module:** configure `@nuxt/fonts` with default weights ([276268d](https://github.com/nuxt/ui/commit/276268d311f57715cec47bc600a0ccc3d3885682))
|
||||
* **NavigationMenu:** arrow position conflict ([#4137](https://github.com/nuxt/ui/issues/4137)) ([0dc4678](https://github.com/nuxt/ui/commit/0dc4678c68e4b500be49c38336dc75b73843e38d))
|
||||
* **Select:** support more primitive types in `value` field ([#4105](https://github.com/nuxt/ui/issues/4105)) ([09b4699](https://github.com/nuxt/ui/commit/09b4699aeadaa195ea081509f8e237bb2c346238))
|
||||
* **Slider:** handle generic types ([d7a4d02](https://github.com/nuxt/ui/commit/d7a4d029b77d2dfa0b8efcd2755d482fa5e31fd3))
|
||||
* **Stepper:** use `div` tag for `title` & `description` ([a57844e](https://github.com/nuxt/ui/commit/a57844e41676c13ed1af861424961b88cee7b4da)), closes [#4096](https://github.com/nuxt/ui/issues/4096)
|
||||
* **Tabs:** prevent trigger truncate without parent width ([06e5689](https://github.com/nuxt/ui/commit/06e5689da80b36205d0548d5d6b58510938e4a6e)), closes [#4056](https://github.com/nuxt/ui/issues/4056)
|
||||
* **Tabs:** set `focus:outline-none` with `link` variant ([999a0f8](https://github.com/nuxt/ui/commit/999a0f84671fad20fa3dc50c6774af2e0200b32e))
|
||||
* **templates:** dont write unused variants in theme files ([d3df3bb](https://github.com/nuxt/ui/commit/d3df3bb929fe6732f27b182d1664213884a662ec))
|
||||
* **Toaster:** allow `base` slot override ([c63d2f3](https://github.com/nuxt/ui/commit/c63d2f380aac16f1d1e812516df3dca7fa7c8034))
|
||||
* **vue:** make `useAppConfig` reactive ([869c070](https://github.com/nuxt/ui/commit/869c0708bd351c7be44e5e430c348b19dd316db9)), closes [#3952](https://github.com/nuxt/ui/issues/3952)
|
||||
|
||||
## [3.1.1](https://github.com/nuxt/ui/compare/v3.1.0...v3.1.1) (2025-05-02)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -66,7 +66,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -109,7 +109,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<${upperName}Root v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })" />
|
||||
<${upperName}Root v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })" />
|
||||
</template>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ provide('navigation', mappedNavigation)
|
||||
<NuxtLoadingIndicator color="var(--ui-primary)" :height="2" />
|
||||
|
||||
<template v-if="!route.path.startsWith('/examples')">
|
||||
<Banner />
|
||||
<!-- <Banner /> -->
|
||||
|
||||
<Header :links="links" />
|
||||
</template>
|
||||
|
||||
@@ -20,7 +20,7 @@ watch(framework, () => {
|
||||
color="neutral"
|
||||
:ui="{
|
||||
indicator: 'bg-default',
|
||||
trigger: 'px-1 data-[state=active]:text-highlighted'
|
||||
trigger: 'px-1 data-[state=active]:text-highlighted w-full'
|
||||
}"
|
||||
size="xs"
|
||||
@update:model-value="(framework = $event as string)"
|
||||
|
||||
@@ -20,7 +20,7 @@ watch(module, () => {
|
||||
color="neutral"
|
||||
:ui="{
|
||||
indicator: 'bg-default',
|
||||
trigger: 'px-1 data-[state=active]:text-highlighted'
|
||||
trigger: 'px-1 data-[state=active]:text-highlighted w-full'
|
||||
}"
|
||||
size="xs"
|
||||
@update:model-value="(module = $event as string)"
|
||||
|
||||
@@ -38,7 +38,7 @@ const schemaProps = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProseCollapsible v-if="schemaProps?.length" class="mt-1">
|
||||
<ProseCollapsible v-if="schemaProps?.length" class="mt-1 mb-0">
|
||||
<ProseUl>
|
||||
<ProseLi v-for="schemaProp in schemaProps" :key="schemaProp.name">
|
||||
<HighlightInlineType :type="`${schemaProp.name}${schemaProp.required === false ? '?' : ''}: ${schemaProp.type}`" />
|
||||
|
||||
@@ -26,6 +26,7 @@ function getEmojiFlag(locale: string): string {
|
||||
km: 'kh', // Khmer -> Cambodia
|
||||
ko: 'kr', // Korean -> South Korea
|
||||
nb: 'no', // Norwegian Bokmål -> Norway
|
||||
sl: 'si', // Slovenian -> Slovenia
|
||||
sv: 'se', // Swedish -> Sweden
|
||||
uk: 'ua', // Ukrainian -> Ukraine
|
||||
ur: 'pk', // Urdu -> Pakistan
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
|
||||
const searchTerm = ref('')
|
||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'command-palette-users',
|
||||
params: { q: searchTermDebounced },
|
||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import type { AvatarProps } from '@nuxt/ui'
|
||||
|
||||
const searchTerm = ref('')
|
||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users',
|
||||
params: { q: searchTermDebounced },
|
||||
transform: (data: { id: number, name: string }[]) => {
|
||||
return data?.map(user => ({
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import type { AvatarProps } from '@nuxt/ui'
|
||||
|
||||
const searchTerm = ref('')
|
||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users',
|
||||
params: { q: searchTermDebounced },
|
||||
transform: (data: { id: number, name: string }[]) => {
|
||||
return data?.map(user => ({
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { h, resolveComponent } from 'vue'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
import { getGroupedRowModel, type GroupingOptions } from '@tanstack/vue-table'
|
||||
|
||||
const UBadge = resolveComponent('UBadge')
|
||||
|
||||
type Account = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type PaymentStatus = 'paid' | 'failed' | 'refunded'
|
||||
|
||||
type Payment = {
|
||||
id: string
|
||||
date: string
|
||||
status: PaymentStatus
|
||||
email: string
|
||||
amount: number
|
||||
account: Account
|
||||
}
|
||||
|
||||
const getColorByStatus = (status: PaymentStatus) => {
|
||||
return {
|
||||
paid: 'success',
|
||||
failed: 'error',
|
||||
refunded: 'neutral'
|
||||
}[status]
|
||||
}
|
||||
|
||||
const data = ref<Payment[]>([
|
||||
{
|
||||
id: '4600',
|
||||
date: '2024-03-11T15:30:00',
|
||||
status: 'paid',
|
||||
email: 'james.anderson@example.com',
|
||||
amount: 594,
|
||||
account: {
|
||||
id: '1',
|
||||
name: 'Account 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4599',
|
||||
date: '2024-03-11T10:10:00',
|
||||
status: 'failed',
|
||||
email: 'mia.white@example.com',
|
||||
amount: 276,
|
||||
account: {
|
||||
id: '2',
|
||||
name: 'Account 2'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4598',
|
||||
date: '2024-03-11T08:50:00',
|
||||
status: 'refunded',
|
||||
email: 'william.brown@example.com',
|
||||
amount: 315,
|
||||
account: {
|
||||
id: '1',
|
||||
name: 'Account 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4597',
|
||||
date: '2024-03-10T19:45:00',
|
||||
status: 'paid',
|
||||
email: 'emma.davis@example.com',
|
||||
amount: 529,
|
||||
account: {
|
||||
id: '2',
|
||||
name: 'Account 2'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4596',
|
||||
date: '2024-03-10T15:55:00',
|
||||
status: 'paid',
|
||||
email: 'ethan.harris@example.com',
|
||||
amount: 639,
|
||||
account: {
|
||||
id: '1',
|
||||
name: 'Account 1'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const columns: TableColumn<Payment>[] = [
|
||||
{
|
||||
id: 'title',
|
||||
header: 'Item'
|
||||
},
|
||||
{
|
||||
id: 'account_id',
|
||||
accessorKey: 'account.id'
|
||||
},
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: '#',
|
||||
cell: ({ row }) =>
|
||||
row.getIsGrouped()
|
||||
? `${row.getValue('id')} records`
|
||||
: `#${row.getValue('id')}`,
|
||||
aggregationFn: 'count'
|
||||
},
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: 'Date',
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.getValue('date')).toLocaleString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
},
|
||||
aggregationFn: 'max'
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status'
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: 'Email',
|
||||
meta: {
|
||||
class: {
|
||||
td: 'w-full'
|
||||
}
|
||||
},
|
||||
cell: ({ row }) =>
|
||||
row.getIsGrouped()
|
||||
? `${row.getValue('email')} customers`
|
||||
: row.getValue('email'),
|
||||
aggregationFn: 'uniqueCount'
|
||||
},
|
||||
{
|
||||
accessorKey: 'amount',
|
||||
header: () => h('div', { class: 'text-right' }, 'Amount'),
|
||||
cell: ({ row }) => {
|
||||
const amount = Number.parseFloat(row.getValue('amount'))
|
||||
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount)
|
||||
|
||||
return h('div', { class: 'text-right font-medium' }, formatted)
|
||||
},
|
||||
aggregationFn: 'sum'
|
||||
}
|
||||
]
|
||||
|
||||
const grouping_options = ref<GroupingOptions>({
|
||||
groupedColumnMode: 'remove',
|
||||
getGroupedRowModel: getGroupedRowModel()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:grouping="['account_id', 'status']"
|
||||
:grouping-options="grouping_options"
|
||||
:ui="{
|
||||
root: 'min-w-full',
|
||||
td: 'empty:p-0' // helps with the colspaned row added for expand slot
|
||||
}"
|
||||
>
|
||||
<template #title-cell="{ row }">
|
||||
<div v-if="row.getIsGrouped()" class="flex items-center">
|
||||
<span
|
||||
class="inline-block"
|
||||
:style="{ width: `calc(${row.depth} * 1rem)` }"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="neutral"
|
||||
class="mr-2"
|
||||
size="xs"
|
||||
:icon="row.getIsExpanded() ? 'i-lucide-minus' : 'i-lucide-plus'"
|
||||
@click="row.toggleExpanded()"
|
||||
/>
|
||||
<strong v-if="row.groupingColumnId === 'account_id'">{{
|
||||
row.original.account.name
|
||||
}}</strong>
|
||||
<UBadge
|
||||
v-else-if="row.groupingColumnId === 'status'"
|
||||
:color="getColorByStatus(row.original.status)"
|
||||
class="capitalize"
|
||||
variant="subtle"
|
||||
>
|
||||
{{ row.original.status }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
@@ -26,7 +26,7 @@ const state = reactive({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" variant="link" class="gap-4 w-full" :ui="{ trigger: 'flex-1' }">
|
||||
<UTabs :items="items" variant="link" class="gap-4 w-full" :ui="{ trigger: 'grow' }">
|
||||
<template #account="{ item }">
|
||||
<p class="text-muted mb-4">
|
||||
{{ item.description }}
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabsItem } from '@nuxt/ui'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const items: TabsItem[] = [
|
||||
{
|
||||
label: 'Account'
|
||||
label: 'Account',
|
||||
value: 'account'
|
||||
},
|
||||
{
|
||||
label: 'Password'
|
||||
label: 'Password',
|
||||
value: 'password'
|
||||
}
|
||||
]
|
||||
|
||||
const active = ref('0')
|
||||
|
||||
// Note: This is for demonstration purposes only. Don't do this at home.
|
||||
onMounted(() => {
|
||||
setInterval(() => {
|
||||
active.value = String((Number(active.value) + 1) % items.length)
|
||||
}, 2000)
|
||||
const active = computed({
|
||||
get() {
|
||||
return (route.query.tab as string) || 'account'
|
||||
},
|
||||
set(tab) {
|
||||
// Hash is specified here to prevent the page from scrolling to the top
|
||||
router.push({
|
||||
path: '/components/tabs',
|
||||
query: { tab },
|
||||
hash: '#control-active-item'
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ export function useLinks() {
|
||||
label: 'Raycast Extension',
|
||||
description: 'Access Nuxt UI components without leaving your editor.',
|
||||
icon: 'i-simple-icons-raycast',
|
||||
to: 'https://www.raycast.com/HugoRCD/nuxt-ui',
|
||||
to: 'https://www.raycast.com/HugoRCD/nuxt',
|
||||
target: '_blank'
|
||||
}, {
|
||||
label: 'Figma to Code',
|
||||
|
||||
@@ -59,7 +59,7 @@ provide('navigation', mappedNavigation)
|
||||
<UApp>
|
||||
<NuxtLoadingIndicator color="#FFF" />
|
||||
|
||||
<Banner />
|
||||
<!-- <Banner /> -->
|
||||
|
||||
<Header :links="links" />
|
||||
|
||||
|
||||
@@ -65,13 +65,17 @@ if (!import.meta.prerender) {
|
||||
})
|
||||
}
|
||||
|
||||
const type = page.value?.path.includes('components') ? 'Vue Component ' : page.value?.path.includes('composables') ? 'Vue Composable ' : ''
|
||||
const title = page.value?.navigation?.title ? page.value.navigation.title : page.value?.title
|
||||
const prefix = page.value?.path.includes('components') || page.value?.path.includes('composables') ? 'Vue ' : ''
|
||||
const suffix = page.value?.path.includes('components') ? 'Component ' : page.value?.path.includes('composables') ? 'Composable ' : ''
|
||||
const description = page.value?.description
|
||||
|
||||
useSeoMeta({
|
||||
titleTemplate: `%s ${type}- Nuxt UI ${page.value.module === 'ui-pro' ? 'Pro' : ''} ${page.value.framework === 'vue' ? ' for Vue' : ''}`,
|
||||
title: page.value.navigation?.title ? page.value.navigation.title : page.value.title,
|
||||
ogTitle: `${page.value.navigation?.title ? page.value.navigation.title : page.value.title} ${type}- Nuxt UI ${page.value.module === 'ui-pro' ? 'Pro' : ''} ${page.value.framework === 'vue' ? ' for Vue' : ''}`,
|
||||
description: page.value.description,
|
||||
ogDescription: page.value.description
|
||||
titleTemplate: `${prefix}%s ${suffix}- Nuxt UI ${page.value?.module === 'ui-pro' ? 'Pro' : ''} ${page.value?.framework === 'vue' ? ' for Vue' : ''}`,
|
||||
title,
|
||||
ogTitle: `${prefix}${title} ${suffix}- Nuxt UI ${page.value?.module === 'ui-pro' ? 'Pro' : ''} ${page.value?.framework === 'vue' ? ' for Vue' : ''}`,
|
||||
description,
|
||||
ogDescription: description
|
||||
})
|
||||
|
||||
if (route.path.startsWith('/components')) {
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const colorMode = useColorMode()
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const name = route.params.slug?.[0]
|
||||
|
||||
if (route.query.theme) {
|
||||
colorMode.preference = route.query.theme === 'light' ? 'light' : 'dark'
|
||||
}
|
||||
if (route.query.neutral) {
|
||||
appConfig.ui.colors.neutral = route.query.neutral as string
|
||||
}
|
||||
if (route.query.primary) {
|
||||
appConfig.ui.colors.primary = route.query.primary as string
|
||||
}
|
||||
|
||||
const width = computed(() => route.query.width && Number.parseInt(route.query.width as string) > 0 ? `${Number.parseInt(route.query.width as string) - 2}px` : '864px')
|
||||
</script>
|
||||
|
||||
|
||||
@@ -24,32 +24,41 @@ onMounted(async () => {
|
||||
const nuxtWordPosition = document.querySelector('#nuxt')?.getBoundingClientRect()
|
||||
const initialScrollX = window.scrollX
|
||||
const initialScrollY = window.scrollY
|
||||
if (figmaWordPosition && nuxtWordPosition) {
|
||||
animate('#cursor1', { left: Math.round(Math.random() * window.outerWidth), top: Math.round(Math.random() * window.outerHeight) }, { duration: 0.1 })
|
||||
.then(() => animate('#cursor1', { opacity: 1 }, { duration: 0.3 }))
|
||||
.then(() => {
|
||||
return animate('#cursor1', {
|
||||
left: Math.round(figmaWordPosition.left + initialScrollX + figmaWordPosition.width / 2),
|
||||
top: Math.round(figmaWordPosition.top + initialScrollY - figmaWordPosition.height / 4)
|
||||
}, { duration: 1.5, delay: 0.2, ease: 'easeInOut' })
|
||||
})
|
||||
.then(() => animate('#cursor1', { scale: 0.8 }, { duration: 0.1, ease: 'easeOut' }))
|
||||
.then(() => animate('#cursor1', { scale: 1 }, { duration: 0.1, ease: 'easeOut' }))
|
||||
.then(() => animate('#figma', { color: 'var(--ui-info)' }, { duration: 0.3, ease: 'easeOut' }))
|
||||
.then(() => animate('#cursor1', { left: Math.round(figmaWordPosition.left + initialScrollX + figmaWordPosition.width), top: Math.round(figmaWordPosition.top + initialScrollY) }, { duration: 0.6, ease: 'easeInOut' }))
|
||||
|
||||
animate('#cursor2', { left: Math.round(Math.random() * window.outerWidth), top: Math.round(Math.random() * window.outerHeight) }, { duration: 0.1, delay: 0.6 })
|
||||
.then(() => animate('#cursor2', { opacity: 1 }, { duration: 0.3 }))
|
||||
.then(() => {
|
||||
return animate('#cursor2', {
|
||||
left: Math.round(nuxtWordPosition.left + initialScrollX + nuxtWordPosition.width / 2),
|
||||
top: Math.round(nuxtWordPosition.top + initialScrollY - nuxtWordPosition.height / 4)
|
||||
}, { duration: 1.5, delay: 0.2, ease: 'easeInOut' })
|
||||
})
|
||||
.then(() => animate('#cursor2', { scale: 0.8 }, { duration: 0.1, ease: 'easeOut' }))
|
||||
.then(() => animate('#cursor2', { scale: 1 }, { duration: 0.1, ease: 'easeOut' }))
|
||||
.then(() => animate('#nuxt', { color: 'var(--ui-success)' }, { duration: 0.3, ease: 'easeOut' }))
|
||||
.then(() => animate('#cursor2', { left: Math.round(nuxtWordPosition.left + initialScrollX + nuxtWordPosition.width), top: Math.round(nuxtWordPosition.top + initialScrollY) }, { duration: 0.6, ease: 'easeInOut' }))
|
||||
if (figmaWordPosition && nuxtWordPosition) {
|
||||
const cursor1Sequence = async () => {
|
||||
await animate('#cursor1', { left: Math.round(Math.random() * window.outerWidth), top: Math.round(Math.random() * window.outerHeight) }, { duration: 0.1 }).finished
|
||||
await animate('#cursor1', { opacity: 1 }, { duration: 0.3 }).finished
|
||||
await animate('#cursor1', {
|
||||
left: Math.round(figmaWordPosition.left + initialScrollX + figmaWordPosition.width / 2),
|
||||
top: Math.round(figmaWordPosition.top + initialScrollY - figmaWordPosition.height / 4)
|
||||
}, { duration: 1.5, delay: 0.2, ease: 'easeInOut' }).finished
|
||||
await animate('#cursor1', { scale: 0.8 }, { duration: 0.1, ease: 'easeOut' }).finished
|
||||
await animate('#cursor1', { scale: 1 }, { duration: 0.1, ease: 'easeOut' }).finished
|
||||
await animate('#figma', { color: 'var(--ui-info)' }, { duration: 0.3, ease: 'easeOut' }).finished
|
||||
await animate('#cursor1', {
|
||||
left: Math.round(figmaWordPosition.left + initialScrollX + figmaWordPosition.width),
|
||||
top: Math.round(figmaWordPosition.top + initialScrollY)
|
||||
}, { duration: 0.6, ease: 'easeInOut' }).finished
|
||||
}
|
||||
|
||||
const cursor2Sequence = async () => {
|
||||
await animate('#cursor2', { left: Math.round(Math.random() * window.outerWidth), top: Math.round(Math.random() * window.outerHeight) }, { duration: 0.1, delay: 0.6 }).finished
|
||||
await animate('#cursor2', { opacity: 1 }, { duration: 0.3 }).finished
|
||||
await animate('#cursor2', {
|
||||
left: Math.round(nuxtWordPosition.left + initialScrollX + nuxtWordPosition.width / 2),
|
||||
top: Math.round(nuxtWordPosition.top + initialScrollY - nuxtWordPosition.height / 4)
|
||||
}, { duration: 1.5, delay: 0.2, ease: 'easeInOut' }).finished
|
||||
await animate('#cursor2', { scale: 0.8 }, { duration: 0.1, ease: 'easeOut' }).finished
|
||||
await animate('#cursor2', { scale: 1 }, { duration: 0.1, ease: 'easeOut' }).finished
|
||||
await animate('#nuxt', { color: 'var(--ui-success)' }, { duration: 0.3, ease: 'easeOut' }).finished
|
||||
await animate('#cursor2', {
|
||||
left: Math.round(nuxtWordPosition.left + initialScrollX + nuxtWordPosition.width),
|
||||
top: Math.round(nuxtWordPosition.top + initialScrollY)
|
||||
}, { duration: 0.6, ease: 'easeInOut' }).finished
|
||||
}
|
||||
|
||||
await Promise.all([cursor1Sequence(), cursor2Sequence()])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -239,7 +239,7 @@ useIntersectionObserver(contributorsRef, ([entry]) => {
|
||||
<li>
|
||||
<NuxtLink to="https://github.com/nuxt/ui/graphs/contributors" target="_blank" class="min-w-0">
|
||||
<p class="text-4xl font-semibold text-highlighted truncate">
|
||||
175+
|
||||
200+
|
||||
</p>
|
||||
<p class="text-muted text-sm truncate">Contributors</p>
|
||||
</NuxtLink>
|
||||
|
||||
@@ -55,6 +55,7 @@ const icons = {
|
||||
:title="user.name"
|
||||
:description="[user.pronouns, user.location].filter(Boolean).join(' ・ ')"
|
||||
:ui="{
|
||||
wrapper: 'items-center',
|
||||
container: 'gap-y-4 lg:p-8',
|
||||
leading: 'flex justify-center',
|
||||
title: 'text-center',
|
||||
@@ -123,6 +124,7 @@ const icons = {
|
||||
:key="contributor.username"
|
||||
:title="contributor.username"
|
||||
:ui="{
|
||||
wrapper: 'items-center',
|
||||
container: 'gap-y-2',
|
||||
leading: 'flex justify-center',
|
||||
title: 'text-center',
|
||||
|
||||
@@ -229,6 +229,10 @@ export default defineConfig({
|
||||
|
||||
::
|
||||
|
||||
::caution
|
||||
When configuring your theme colors, you must use either color names from the [default Tailwind palette](https://tailwindcss.com/docs/colors) (like 'blue', 'green', etc.) or reference custom colors that you've previously defined in your [CSS file](#theme).
|
||||
::
|
||||
|
||||
### Extend colors
|
||||
|
||||
::framework-only
|
||||
@@ -973,7 +977,7 @@ export default {
|
||||
|
||||
```vue [src/runtime/components/Card.vue]
|
||||
<template>
|
||||
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<div :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<div :class="ui.header({ class: props.ui?.header })">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ Here's an overview of the key directories and files in the Nuxt UI project struc
|
||||
|
||||
### Documentation
|
||||
|
||||
The documentation lives in the `docs` folder as a Nuxt app using `@nuxt/content` v3 to generate pages from Markdown files. See the [Content v3 Docs](https://content3.nuxt.dev/docs/getting-started) for details on how it works. Here's a breakdown of its structure:
|
||||
The documentation lives in the `docs` folder as a Nuxt app using `@nuxt/content` v3 to generate pages from Markdown files. See the [Nuxt Content documentation](https://content.nuxt.com/docs/getting-started) for details on how it works. Here's a breakdown of its structure:
|
||||
|
||||
```bash
|
||||
├── app/
|
||||
|
||||
@@ -23,6 +23,8 @@ Use the `items` prop as an array of objects with the following properties:
|
||||
- `value?: string`{lang="ts-type"}
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue, header?: ClassNameValue, trigger?: ClassNameValue, leadingIcon?: ClassNameValue, label?: ClassNameValue, trailingIcon?: ClassNameValue, content?: ClassNameValue, body?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
::component-code
|
||||
---
|
||||
|
||||
@@ -16,8 +16,9 @@ Use the `items` prop as an array of objects with the following properties:
|
||||
- `label?: string`{lang="ts-type"}
|
||||
- `icon?: string`{lang="ts-type"}
|
||||
- `avatar?: AvatarProps`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue, link?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLeadingAvatar?: ClassNameValue, linkLabel?: ClassNameValue, separator?: ClassNameValue, separatorIcon?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ class: 'p-8'
|
||||
---
|
||||
::
|
||||
|
||||
You can also pass an array of objects with the following properties:
|
||||
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
You can control how many items are visible by using the [`basis`](https://tailwindcss.com/docs/flex-basis) / [`width`](https://tailwindcss.com/docs/width) utility classes on the `item`:
|
||||
|
||||
::component-example
|
||||
|
||||
@@ -49,6 +49,8 @@ You can also pass an array of objects with the following properties:
|
||||
- `description?: string`{lang="ts-type"}
|
||||
- [`value?: string`{lang="ts-type"}](#value-key)
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, base?: ClassNameValue, 'indicator'?: ClassNameValue, icon?: ClassNameValue, wrapper?: ClassNameValue, label?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
::component-code
|
||||
---
|
||||
@@ -199,6 +201,7 @@ items:
|
||||
variant:
|
||||
- list
|
||||
- card
|
||||
- table
|
||||
props:
|
||||
color: 'primary'
|
||||
variant: 'card'
|
||||
@@ -229,6 +232,7 @@ items:
|
||||
variant:
|
||||
- list
|
||||
- card
|
||||
- table
|
||||
props:
|
||||
size: 'xl'
|
||||
variant: 'list'
|
||||
@@ -259,6 +263,7 @@ items:
|
||||
variant:
|
||||
- list
|
||||
- card
|
||||
- table
|
||||
props:
|
||||
orientation: 'horizontal'
|
||||
variant: 'list'
|
||||
@@ -293,6 +298,7 @@ items:
|
||||
variant:
|
||||
- list
|
||||
- card
|
||||
- table
|
||||
props:
|
||||
indicator: 'end'
|
||||
variant: 'card'
|
||||
|
||||
@@ -53,6 +53,8 @@ Each group contains an `items` array of objects that define the commands. Each i
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||
- `onSelect?(e?: Event): void`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelPrefix?: ClassNameValue, itemLabelBase?: ClassNameValue, itemLabelSuffix?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue, itemTrailingHighlightedIcon?: ClassNameValue, itemTrailingIcon?: ClassNameValue,}`{lang="ts-type"}
|
||||
|
||||
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
|
||||
|
||||
|
||||
@@ -28,11 +28,12 @@ Use the `items` prop as an array of objects with the following properties:
|
||||
- [`color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{lang="ts-type"}](#with-color-items)
|
||||
- [`checked?: boolean`{lang="ts-type"}](#with-checkbox-items)
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||
- `onSelect?(e: Event): void`{lang="ts-type"}
|
||||
- [`onUpdateChecked?(checked: boolean): void`{lang="ts-type"}](#with-checkbox-items)
|
||||
- `children?: ContextMenuItem[] | ContextMenuItem[][]`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelExternalIcon?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
|
||||
|
||||
|
||||
@@ -28,11 +28,12 @@ Use the `items` prop as an array of objects with the following properties:
|
||||
- [`color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{lang="ts-type"}](#with-color-items)
|
||||
- [`checked?: boolean`{lang="ts-type"}](#with-checkbox-items)
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||
- `onSelect?(e: Event): void`{lang="ts-type"}
|
||||
- [`onUpdateChecked?(checked: boolean): void`{lang="ts-type"}](#with-checkbox-items)
|
||||
- `children?: DropdownMenuItem[] | DropdownMenuItem[][]`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelExternalIcon?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ It requires two props:
|
||||
**No validation library is included** by default, ensure you **install the one you need**.
|
||||
::
|
||||
|
||||
::tabs
|
||||
::tabs{class="gap-0"}
|
||||
::component-example{label="Valibot"}
|
||||
---
|
||||
name: 'form-example-valibot'
|
||||
|
||||
@@ -55,6 +55,8 @@ You can also pass an array of objects with the following properties:
|
||||
- [`chip?: ChipProps`{lang="ts-type"}](#with-chip-in-items)
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- `onSelect?(e: Event): void`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { tagsItem?: ClassNameValue, tagsItemText?: ClassNameValue, tagsItemDelete?: ClassNameValue, tagsItemDeleteIcon?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
::component-code
|
||||
---
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
---
|
||||
title: Keyboard Key
|
||||
title: Kbd
|
||||
description: A kbd element to display a keyboard key.
|
||||
category: element
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Kbd.vue
|
||||
navigation:
|
||||
title: Kbd
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -21,14 +21,19 @@ Use the `items` prop as an array of objects with the following properties:
|
||||
- `icon?: string`{lang="ts-type"}
|
||||
- `avatar?: AvatarProps`{lang="ts-type"}
|
||||
- `badge?: string | number | BadgeProps`{lang="ts-type"}
|
||||
- `tooltip?: TooltipProps`{lang="ts-type"} :badge{label="Soon"}
|
||||
- `trailingIcon?: string`{lang="ts-type"}
|
||||
- `type?: 'label' | 'link'`{lang="ts-type"}
|
||||
- `collapsible?: boolean`{lang="ts-type"} :badge{label="Soon"}
|
||||
- `defaultOpen?: boolean`{lang="ts-type"}
|
||||
- `open?: boolean`{lang="ts-type"}
|
||||
- `value?: string`{lang="ts-type"}
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||
- `onSelect?(e: Event): void`{lang="ts-type"}
|
||||
- `children?: NavigationMenuChildItem[]`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { linkLeadingAvatarSize?: ClassNameValue, linkLeadingAvatar?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingBadgeSize?: ClassNameValue, linkTrailingBadge?: ClassNameValue, linkTrailingIcon?: ClassNameValue, label?: ClassNameValue, link?: ClassNameValue, content?: ClassNameValue, childList?: ClassNameValue, childItem?: ClassNameValue, childLink?: ClassNameValue, childLinkIcon?: ClassNameValue, childLinkWrapper?: ClassNameValue, childLinkLabel?: ClassNameValue, childLinkLabelExternalIcon?: ClassNameValue, childLinkDescription?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
|
||||
|
||||
@@ -130,8 +135,8 @@ Each item can take a `children` array of objects with the following properties t
|
||||
- `label: string`
|
||||
- `description?: string`
|
||||
- `icon?: string`
|
||||
- `class?: any`
|
||||
- `onSelect?(e: Event): void`
|
||||
- `class?: any`
|
||||
|
||||
::
|
||||
|
||||
@@ -237,6 +242,108 @@ props:
|
||||
Groups will be spaced when orientation is `horizontal` and separated when orientation is `vertical`.
|
||||
::
|
||||
|
||||
### Collapsed
|
||||
|
||||
Use the `collapsed` prop to collapse the NavigationMenu, this can be useful in a sidebar for example.
|
||||
|
||||
::component-code
|
||||
---
|
||||
collapse: true
|
||||
ignore:
|
||||
- items
|
||||
- orientation
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[][]
|
||||
props:
|
||||
collapsed: true
|
||||
orientation: 'vertical'
|
||||
items:
|
||||
- - label: Links
|
||||
type: 'label'
|
||||
- label: Guide
|
||||
icon: i-lucide-book-open
|
||||
children:
|
||||
- label: Introduction
|
||||
description: Fully styled and customizable components for Nuxt.
|
||||
icon: i-lucide-house
|
||||
- label: Installation
|
||||
description: Learn how to install and configure Nuxt UI in your application.
|
||||
icon: i-lucide-cloud-download
|
||||
- label: 'Icons'
|
||||
icon: 'i-lucide-smile'
|
||||
description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
|
||||
- label: 'Colors'
|
||||
icon: 'i-lucide-swatch-book'
|
||||
description: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
|
||||
- label: 'Theme'
|
||||
icon: 'i-lucide-cog'
|
||||
description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
|
||||
- label: Composables
|
||||
icon: i-lucide-database
|
||||
collapsible: false
|
||||
open: false
|
||||
children:
|
||||
- label: defineShortcuts
|
||||
icon: i-lucide-file-text
|
||||
description: Define shortcuts for your application.
|
||||
to: /composables/define-shortcuts
|
||||
- label: useOverlay
|
||||
icon: i-lucide-file-text
|
||||
description: Display a modal/slideover within your application.
|
||||
to: /composables/use-overlay
|
||||
- label: useToast
|
||||
icon: i-lucide-file-text
|
||||
description: Display a toast within your application.
|
||||
to: /composables/use-toast
|
||||
- label: Components
|
||||
icon: i-lucide-box
|
||||
collapsible: false
|
||||
open: false
|
||||
to: /components
|
||||
active: true
|
||||
children:
|
||||
- label: Link
|
||||
icon: i-lucide-file-text
|
||||
description: Use NuxtLink with superpowers.
|
||||
to: /components/link
|
||||
- label: Modal
|
||||
icon: i-lucide-file-text
|
||||
description: Display a modal within your application.
|
||||
to: /components/modal
|
||||
- label: NavigationMenu
|
||||
icon: i-lucide-file-text
|
||||
description: Display a list of links.
|
||||
to: /components/navigation-menu
|
||||
- label: Pagination
|
||||
icon: i-lucide-file-text
|
||||
description: Display a list of pages.
|
||||
to: /components/pagination
|
||||
- label: Popover
|
||||
icon: i-lucide-file-text
|
||||
description: Display a non-modal dialog that floats around a trigger element.
|
||||
to: /components/popover
|
||||
- label: Progress
|
||||
icon: i-lucide-file-text
|
||||
description: Show a horizontal bar to indicate task progression.
|
||||
to: /components/progress
|
||||
- - label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
badge: 3.8k
|
||||
to: https://github.com/nuxt/ui
|
||||
target: _blank
|
||||
- label: Help
|
||||
icon: i-lucide-circle-help
|
||||
disabled: true
|
||||
---
|
||||
::
|
||||
|
||||
::tip
|
||||
You can set the `collapsible: false` property on items with children to prevent them from being collapsible. This allows the item to act as a regular link while still displaying its children in a submenu.
|
||||
::
|
||||
|
||||
### Highlight
|
||||
|
||||
Use the `highlight` prop to display a highlighted border for the active item.
|
||||
@@ -778,6 +885,126 @@ You can inspect the DOM to see each item's content being rendered.
|
||||
|
||||
## Examples
|
||||
|
||||
### With tooltips in items :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
You can use the `tooltip` property to display a [Tooltip](/components/tooltip) around an item. This can be useful when the menu is collapsed.
|
||||
|
||||
::component-code
|
||||
---
|
||||
collapse: true
|
||||
ignore:
|
||||
- items
|
||||
- orientation
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[][]
|
||||
props:
|
||||
collapsed: true
|
||||
orientation: 'vertical'
|
||||
items:
|
||||
- - label: Links
|
||||
type: 'label'
|
||||
- label: Guide
|
||||
icon: i-lucide-book-open
|
||||
tooltip:
|
||||
text: 'Guide'
|
||||
children:
|
||||
- label: Introduction
|
||||
description: Fully styled and customizable components for Nuxt.
|
||||
icon: i-lucide-house
|
||||
tooltip:
|
||||
text: 'Introduction'
|
||||
- label: Installation
|
||||
description: Learn how to install and configure Nuxt UI in your application.
|
||||
icon: i-lucide-cloud-download
|
||||
tooltip:
|
||||
text: 'Installation'
|
||||
- label: 'Icons'
|
||||
icon: 'i-lucide-smile'
|
||||
description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
|
||||
tooltip:
|
||||
text: 'Icons'
|
||||
- label: 'Colors'
|
||||
icon: 'i-lucide-swatch-book'
|
||||
description: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
|
||||
tooltip:
|
||||
text: 'Colors'
|
||||
- label: 'Theme'
|
||||
icon: 'i-lucide-cog'
|
||||
description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
|
||||
tooltip:
|
||||
text: 'Theme'
|
||||
- label: Composables
|
||||
icon: i-lucide-database
|
||||
tooltip:
|
||||
text: 'Composables'
|
||||
collapsible: false
|
||||
open: false
|
||||
children:
|
||||
- label: defineShortcuts
|
||||
icon: i-lucide-file-text
|
||||
description: Define shortcuts for your application.
|
||||
to: /composables/define-shortcuts
|
||||
- label: useOverlay
|
||||
icon: i-lucide-file-text
|
||||
description: Display a modal/slideover within your application.
|
||||
to: /composables/use-overlay
|
||||
- label: useToast
|
||||
icon: i-lucide-file-text
|
||||
description: Display a toast within your application.
|
||||
to: /composables/use-toast
|
||||
- label: Components
|
||||
icon: i-lucide-box
|
||||
tooltip:
|
||||
text: 'Components'
|
||||
to: /components
|
||||
active: true
|
||||
collapsible: false
|
||||
open: false
|
||||
children:
|
||||
- label: Link
|
||||
icon: i-lucide-file-text
|
||||
description: Use NuxtLink with superpowers.
|
||||
to: /components/link
|
||||
- label: Modal
|
||||
icon: i-lucide-file-text
|
||||
description: Display a modal within your application.
|
||||
to: /components/modal
|
||||
- label: NavigationMenu
|
||||
icon: i-lucide-file-text
|
||||
description: Display a list of links.
|
||||
to: /components/navigation-menu
|
||||
- label: Pagination
|
||||
icon: i-lucide-file-text
|
||||
description: Display a list of pages.
|
||||
to: /components/pagination
|
||||
- label: Popover
|
||||
icon: i-lucide-file-text
|
||||
description: Display a non-modal dialog that floats around a trigger element.
|
||||
to: /components/popover
|
||||
- label: Progress
|
||||
icon: i-lucide-file-text
|
||||
description: Show a horizontal bar to indicate task progression.
|
||||
to: /components/progress
|
||||
- - label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
badge: 3.8k
|
||||
to: https://github.com/nuxt/ui
|
||||
target: _blank
|
||||
tooltip:
|
||||
text: 'GitHub'
|
||||
kbds:
|
||||
- 3.8k
|
||||
- label: Help
|
||||
icon: i-lucide-circle-help
|
||||
disabled: true
|
||||
tooltip:
|
||||
text: 'Help'
|
||||
---
|
||||
::
|
||||
|
||||
### Control active item
|
||||
|
||||
You can control the active item by using the `default-value` prop or the `v-model` directive with the index of the item.
|
||||
|
||||
@@ -46,6 +46,8 @@ You can also pass an array of objects with the following properties:
|
||||
- `description?: string`{lang="ts-type"}
|
||||
- [`value?: string`{lang="ts-type"}](#value-key)
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, base?: ClassNameValue, 'indicator'?: ClassNameValue, wrapper?: ClassNameValue, label?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
::component-code
|
||||
---
|
||||
|
||||
@@ -57,6 +57,8 @@ You can also pass an array of objects with the following properties:
|
||||
- [`chip?: ChipProps`{lang="ts-type"}](#with-chip-in-items)
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- `onSelect?(e: Event): void`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
::component-code
|
||||
---
|
||||
|
||||
@@ -48,6 +48,8 @@ You can also pass an array of objects with the following properties:
|
||||
- [`avatar?: AvatarProps`{lang="ts-type"}](#with-avatar-in-items)
|
||||
- [`chip?: ChipProps`{lang="ts-type"}](#with-chip-in-items)
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
::component-code
|
||||
---
|
||||
|
||||
@@ -136,6 +136,21 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Tooltip :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
Use the `tooltip` prop to display a [Tooltip](/components/tooltip) around the Slider thumbs with the current value. You can set it to `true` for default behavior or pass an object to customize it with any property from the [Tooltip](/components/tooltip#props) component.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- defaultValue
|
||||
- tooltip
|
||||
props:
|
||||
defaultValue: 50
|
||||
tooltip: true
|
||||
---
|
||||
::
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` prop to disable the Slider.
|
||||
|
||||
@@ -23,6 +23,8 @@ Use the `items` prop as an array of objects with the following properties:
|
||||
- `value?: string | number`{lang="ts-type"}
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, trigger?: ClassNameValue, indicator?: ClassNameValue, icon?: ClassNameValue, separator?: ClassNameValue, wrapper?: ClassNameValue, title?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
::component-code
|
||||
---
|
||||
|
||||
@@ -260,6 +260,30 @@ You can use the `expanded` prop to control the expandable state of the rows (can
|
||||
You could also add this action to the [`DropdownMenu`](/components/dropdown-menu) component inside the `actions` column.
|
||||
::
|
||||
|
||||
### With grouped rows
|
||||
|
||||
You can group rows based on a given column value and show/hide sub rows via some button added to the cell using the TanStack Table [Grouping APIs](https://tanstack.com/table/latest/docs/api/features/grouping).
|
||||
|
||||
#### Important parts:
|
||||
|
||||
* Add prop `grouping` to `UTable` component with an array of column ids you want to group by.
|
||||
* Add prop `grouping-options` to `UTable`. It must include `getGroupedRowModel`, you can import it from `@tanstack/vue-table` or implement your own.
|
||||
* Expand rows via `row.toggleExpanded()` method on any cell of the row. Keep in mind, it also toggles `#expanded` slot.
|
||||
* Use `aggregateFn` on column definition to define how to aggregate the rows.
|
||||
* `agregatedCell` renderer on column definition only works if there is no `cell` renderer.
|
||||
|
||||
::component-example
|
||||
---
|
||||
prettier: true
|
||||
collapse: true
|
||||
name: 'table-grouped-rows-example'
|
||||
highlights:
|
||||
- 159
|
||||
- 169
|
||||
class: '!p-0'
|
||||
---
|
||||
::
|
||||
|
||||
### With row selection
|
||||
|
||||
You can add a new column that renders a [Checkbox](/components/checkbox) component inside the `header` and `cell` to select rows using the TanStack Table [Row Selection APIs](https://tanstack.com/table/latest/docs/api/features/row-selection).
|
||||
|
||||
@@ -23,6 +23,8 @@ Use the `items` prop as an array of objects with the following properties:
|
||||
- `value?: string | number`{lang="ts-type"}
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { trigger?: ClassNameValue, leadingIcon?: ClassNameValue, leadingAvatar?: ClassNameValue, label?: ClassNameValue, content?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
::component-code
|
||||
---
|
||||
@@ -210,10 +212,6 @@ You can control the active item by using the `default-value` prop or the `v-mode
|
||||
|
||||
:component-example{name="tabs-model-value-example"}
|
||||
|
||||
::tip
|
||||
You can also pass the `value` of one of the items if provided.
|
||||
::
|
||||
|
||||
### With content slot
|
||||
|
||||
Use the `#content` slot to customize the content of each item.
|
||||
|
||||
@@ -26,6 +26,8 @@ Use the `items` prop as an array of objects with the following properties:
|
||||
- `children?: TreeItem[]`{lang="ts-type"}
|
||||
- `onToggle?(e: Event): void`{lang="ts-type"}
|
||||
- `onSelect?(e?: Event): void`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue, itemWithChildren?: ClassNameValue, link?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLabel?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingIcon?: ClassNameValue, listWithChildren?: ClassNameValue }`{lang="ts-type"}
|
||||
|
||||
::note
|
||||
A unique identifier is required for each item. The component will use the `value` prop as identifier, falling back to `label` if `value` is not provided. One of these must be provided for the component to work properly.
|
||||
|
||||
@@ -3,26 +3,26 @@
|
||||
"name": "@nuxt/ui-docs",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@ai-sdk/vue": "^1.2.8",
|
||||
"@ai-sdk/vue": "^1.2.12",
|
||||
"@iconify-json/logos": "^1.2.4",
|
||||
"@iconify-json/lucide": "^1.2.39",
|
||||
"@iconify-json/simple-icons": "^1.2.33",
|
||||
"@iconify-json/vscode-icons": "^1.2.20",
|
||||
"@iconify-json/lucide": "^1.2.43",
|
||||
"@iconify-json/simple-icons": "^1.2.34",
|
||||
"@iconify-json/vscode-icons": "^1.2.21",
|
||||
"@nuxt/content": "^3.5.1",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxt/ui": "latest",
|
||||
"@nuxt/ui-pro": "^3.1.0",
|
||||
"@nuxthub/core": "^0.8.25",
|
||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@a30de4d",
|
||||
"@nuxthub/core": "^0.8.27",
|
||||
"@nuxtjs/plausible": "^1.2.0",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@rollup/plugin-yaml": "^4.1.2",
|
||||
"@vueuse/integrations": "^13.1.0",
|
||||
"@vueuse/nuxt": "^13.1.0",
|
||||
"ai": "^4.3.10",
|
||||
"@vueuse/integrations": "^13.2.0",
|
||||
"@vueuse/nuxt": "^13.2.0",
|
||||
"ai": "^4.3.15",
|
||||
"capture-website": "^4.2.0",
|
||||
"joi": "^17.13.3",
|
||||
"motion-v": "^1.0.0",
|
||||
"nuxt": "^3.17.1",
|
||||
"motion-v": "^1.0.2",
|
||||
"nuxt": "^3.17.3",
|
||||
"nuxt-component-meta": "^0.11.0",
|
||||
"nuxt-llms": "^0.1.2",
|
||||
"nuxt-og-image": "^5.1.3",
|
||||
@@ -31,12 +31,12 @@
|
||||
"sortablejs": "^1.15.6",
|
||||
"superstruct": "^2.0.2",
|
||||
"ufo": "^1.6.1",
|
||||
"valibot": "^1.0.0",
|
||||
"workers-ai-provider": "^0.3.0",
|
||||
"valibot": "^1.1.0",
|
||||
"workers-ai-provider": "^0.4.1",
|
||||
"yup": "^1.6.1",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.13.2"
|
||||
"wrangler": "^4.15.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,33 @@ const parseBoolean = (value?: string): boolean => value === 'true'
|
||||
|
||||
function getComponentMeta(componentName: string) {
|
||||
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
|
||||
const metaComponentName = `U${pascalCaseName}`
|
||||
|
||||
const strategies = [
|
||||
`U${pascalCaseName}`,
|
||||
`Prose${pascalCaseName}`,
|
||||
pascalCaseName
|
||||
]
|
||||
|
||||
let componentMeta: any
|
||||
let finalMetaComponentName: string = pascalCaseName
|
||||
|
||||
for (const nameToTry of strategies) {
|
||||
finalMetaComponentName = nameToTry
|
||||
const metaAttempt = (meta as Record<string, any>)[nameToTry]?.meta
|
||||
if (metaAttempt) {
|
||||
componentMeta = metaAttempt
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!componentMeta) {
|
||||
console.warn(`[getComponentMeta] Metadata not found for ${pascalCaseName} using strategies: U, Prose, or no prefix. Last tried: ${finalMetaComponentName}`)
|
||||
}
|
||||
|
||||
return {
|
||||
pascalCaseName,
|
||||
metaComponentName,
|
||||
componentMeta: (meta as Record<string, any>)[metaComponentName]?.meta
|
||||
metaComponentName: finalMetaComponentName,
|
||||
componentMeta
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +190,7 @@ function emitItemHandler(event: any): string {
|
||||
const generateThemeConfig = ({ pro, prose, componentName }: ThemeConfig) => {
|
||||
const computedTheme = pro ? (prose ? themePro.prose : themePro) : theme
|
||||
const componentTheme = computedTheme[componentName as keyof typeof computedTheme]
|
||||
|
||||
return {
|
||||
[pro ? 'uiPro' : 'ui']: prose
|
||||
? { prose: { [componentName]: componentTheme } }
|
||||
@@ -284,10 +307,14 @@ export default defineNitroPlugin((nitroApp) => {
|
||||
const componentName = camelCase(doc.title)
|
||||
|
||||
visitAndReplace(doc, 'component-theme', (node) => {
|
||||
const attributes = node[1] as ComponentAttributes
|
||||
const attributes = node[1] as Record<string, string>
|
||||
const mdcSpecificName = attributes?.slug
|
||||
|
||||
const finalComponentName = mdcSpecificName ? camelCase(mdcSpecificName) : componentName
|
||||
|
||||
const pro = parseBoolean(attributes[':pro'])
|
||||
const prose = parseBoolean(attributes[':prose'])
|
||||
const appConfig = generateThemeConfig({ pro, prose, componentName })
|
||||
const appConfig = generateThemeConfig({ pro, prose, componentName: finalComponentName })
|
||||
|
||||
replaceNodeWithPre(
|
||||
node,
|
||||
@@ -322,14 +349,23 @@ export default defineNitroPlugin((nitroApp) => {
|
||||
})
|
||||
|
||||
visitAndReplace(doc, 'component-props', (node) => {
|
||||
const { pascalCaseName, componentMeta } = getComponentMeta(componentName)
|
||||
const attributes = node[1] as Record<string, string>
|
||||
const mdcSpecificName = attributes?.name
|
||||
const isProse = parseBoolean(attributes[':prose'])
|
||||
|
||||
const finalComponentName = mdcSpecificName ? camelCase(mdcSpecificName) : componentName
|
||||
|
||||
const { pascalCaseName, componentMeta } = getComponentMeta(finalComponentName)
|
||||
|
||||
if (!componentMeta?.props) return
|
||||
|
||||
const interfaceName = isProse ? `Prose${pascalCaseName}Props` : `${pascalCaseName}Props`
|
||||
|
||||
const interfaceCode = generateTSInterface(
|
||||
`${pascalCaseName}Props`,
|
||||
interfaceName,
|
||||
Object.values(componentMeta.props),
|
||||
propItemHandler,
|
||||
`Props for the ${pascalCaseName} component`
|
||||
`Props for the ${isProse ? 'Prose' : ''}${pascalCaseName} component`
|
||||
)
|
||||
replaceNodeWithPre(node, 'ts', interfaceCode)
|
||||
})
|
||||
|
||||
42
package.json
42
package.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@nuxt/ui",
|
||||
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
|
||||
"version": "3.1.1",
|
||||
"packageManager": "pnpm@10.10.0",
|
||||
"version": "3.1.2",
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/ui.git"
|
||||
@@ -115,18 +115,18 @@
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@internationalized/date": "^3.8.0",
|
||||
"@internationalized/number": "^3.6.1",
|
||||
"@nuxt/fonts": "^0.11.2",
|
||||
"@nuxt/fonts": "^0.11.4",
|
||||
"@nuxt/icon": "^1.12.0",
|
||||
"@nuxt/kit": "^3.17.1",
|
||||
"@nuxt/schema": "^3.17.1",
|
||||
"@nuxt/kit": "^3.17.3",
|
||||
"@nuxt/schema": "^3.17.3",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@tailwindcss/postcss": "^4.1.4",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@tailwindcss/postcss": "^4.1.6",
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@unhead/vue": "^2.0.8",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@vueuse/integrations": "^13.1.0",
|
||||
"@vueuse/core": "^13.2.0",
|
||||
"@vueuse/integrations": "^13.2.0",
|
||||
"colortranslator": "^4.1.0",
|
||||
"consola": "^3.4.2",
|
||||
"defu": "^6.1.4",
|
||||
@@ -144,29 +144,29 @@
|
||||
"mlly": "^1.7.4",
|
||||
"ohash": "^2.0.11",
|
||||
"pathe": "^2.0.3",
|
||||
"reka-ui": "^2.2.0",
|
||||
"reka-ui": "^2.2.1",
|
||||
"scule": "^1.3.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"tinyglobby": "^0.2.13",
|
||||
"unplugin": "^2.3.2",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
"unplugin": "^2.3.4",
|
||||
"unplugin-auto-import": "^19.2.0",
|
||||
"unplugin-vue-components": "^28.5.0",
|
||||
"vaul-vue": "^0.4.1",
|
||||
"vue-component-type-helpers": "^2.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint-config": "^1.3.0",
|
||||
"@nuxt/eslint-config": "^1.3.1",
|
||||
"@nuxt/module-builder": "^1.0.1",
|
||||
"@nuxt/test-utils": "^3.17.2",
|
||||
"@nuxt/test-utils": "^3.18.0",
|
||||
"@release-it/conventional-changelog": "^10.0.1",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"embla-carousel": "^8.6.0",
|
||||
"eslint": "^9.25.1",
|
||||
"happy-dom": "^17.4.4",
|
||||
"nuxt": "^3.17.1",
|
||||
"release-it": "^19.0.1",
|
||||
"vitest": "^3.1.2",
|
||||
"eslint": "^9.26.0",
|
||||
"happy-dom": "^17.4.7",
|
||||
"nuxt": "^3.17.3",
|
||||
"release-it": "^19.0.2",
|
||||
"vitest": "^3.1.3",
|
||||
"vitest-environment-nuxt": "^1.0.1",
|
||||
"vue-tsc": "^2.2.10"
|
||||
},
|
||||
@@ -209,7 +209,7 @@
|
||||
"debug": "4.3.7",
|
||||
"rollup": "4.34.9",
|
||||
"unimport": "4.1.1",
|
||||
"unplugin": "^2.3.2"
|
||||
"unplugin": "^2.3.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=public-sans:400,500,600,700" rel="stylesheet" />
|
||||
<title>Nuxt UI - Vue Playground</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/ui": "latest",
|
||||
"vue": "^3.5.13",
|
||||
"vue": "^3.5.14",
|
||||
"vue-router": "^4.5.1",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.3",
|
||||
"vite": "^6.3.5",
|
||||
"vue-tsc": "^2.2.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,14 +36,27 @@ const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme
|
||||
color="neutral"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ms-[-56px]">
|
||||
<div class="flex items-center gap-2 ms-[-90px]">
|
||||
<UBadge v-for="size in sizes" :key="size" label="Badge" :size="size" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ms-[-86px]">
|
||||
<div class="flex items-center gap-2 ms-[-122px]">
|
||||
<UBadge v-for="size in sizes" :key="size" icon="i-lucide-rocket" label="Badge" :size="size" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ms-[-86px]">
|
||||
<div class="flex items-center gap-2 ms-[-130px]">
|
||||
<UBadge v-for="size in sizes" :key="size" :avatar="{ src: 'https://github.com/benjamincanac.png' }" label="Badge" :size="size" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ms-[-52px]">
|
||||
<UBadge v-for="size in sizes" :key="size" icon="i-lucide-rocket" :size="size" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ms-[-60px]">
|
||||
<UBadge
|
||||
v-for="size in sizes"
|
||||
:key="size"
|
||||
:avatar="{ src: 'https://github.com/benjamincanac.png' }"
|
||||
:size="size"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import themeCheckbox from '#build/ui/checkbox'
|
||||
import theme from '#build/ui/checkbox-group'
|
||||
|
||||
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size>
|
||||
const variants = Object.keys(themeCheckbox.variants.variant)
|
||||
const variants = Object.keys(theme.variants.variant)
|
||||
const variant = ref('list' as const)
|
||||
|
||||
const literalOptions = [
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
"typecheck": "nuxt typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.39",
|
||||
"@iconify-json/simple-icons": "^1.2.33",
|
||||
"@iconify-json/lucide": "^1.2.43",
|
||||
"@iconify-json/simple-icons": "^1.2.34",
|
||||
"@nuxt/ui": "latest",
|
||||
"@nuxthub/core": "^0.8.25",
|
||||
"nuxt": "^3.17.1",
|
||||
"zod": "^3.24.3"
|
||||
"@nuxthub/core": "^0.8.27",
|
||||
"nuxt": "^3.17.3",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
|
||||
4575
pnpm-lock.yaml
generated
4575
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,6 @@
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
},
|
||||
"ignoreDeps": [
|
||||
"vue-tsc"
|
||||
],
|
||||
"baseBranches": ["v2", "v3"],
|
||||
"packageRules": [{
|
||||
"matchBaseBranches": ["v3"],
|
||||
@@ -19,6 +16,11 @@
|
||||
"@tailwindcss/postcss",
|
||||
"@tailwindcss/vite"
|
||||
]
|
||||
}, {
|
||||
"groupName": "reka-ui",
|
||||
"matchPackageNames": [
|
||||
"reka-ui"
|
||||
]
|
||||
}, {
|
||||
"matchDepTypes": ["peerDependencies"],
|
||||
"enabled": false
|
||||
|
||||
@@ -91,12 +91,21 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
}
|
||||
}
|
||||
|
||||
await registerModule('@nuxt/icon', 'icon', { cssLayer: 'components' })
|
||||
await registerModule('@nuxt/icon', 'icon', {
|
||||
cssLayer: 'components'
|
||||
})
|
||||
if (options.fonts) {
|
||||
await registerModule('@nuxt/fonts', 'fonts', { experimental: { processCSSVariables: true } })
|
||||
await registerModule('@nuxt/fonts', 'fonts', {
|
||||
defaults: {
|
||||
weights: [400, 500, 600, 700]
|
||||
}
|
||||
})
|
||||
}
|
||||
if (options.colorMode) {
|
||||
await registerModule('@nuxtjs/color-mode', 'colorMode', { classSuffix: '', disableTransition: true })
|
||||
await registerModule('@nuxtjs/color-mode', 'colorMode', {
|
||||
classSuffix: '',
|
||||
disableTransition: true
|
||||
})
|
||||
}
|
||||
|
||||
addPlugin({ src: resolve('./runtime/plugins/colors') })
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface AccordionItem {
|
||||
/** A unique value for the accordion item. Defaults to the index. */
|
||||
value?: string
|
||||
disabled?: boolean
|
||||
class?: any
|
||||
ui?: Pick<Accordion['slots'], 'item' | 'header' | 'trigger' | 'leadingIcon' | 'label' | 'trailingIcon' | 'content' | 'body'>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -89,34 +91,34 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.accordion ||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<AccordionRoot v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<AccordionItem
|
||||
v-for="(item, index) in props.items"
|
||||
v-slot="{ open }"
|
||||
:key="index"
|
||||
:value="item.value || String(index)"
|
||||
:disabled="item.disabled"
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })"
|
||||
>
|
||||
<AccordionHeader as="div" :class="ui.header({ class: props.ui?.header })">
|
||||
<AccordionTrigger :class="ui.trigger({ class: props.ui?.trigger, disabled: item.disabled })">
|
||||
<AccordionHeader as="div" :class="ui.header({ class: [props.ui?.header, item.ui?.header] })">
|
||||
<AccordionTrigger :class="ui.trigger({ class: [props.ui?.trigger, item.ui?.trigger], disabled: item.disabled })">
|
||||
<slot name="leading" :item="item" :index="index" :open="open">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: [props.ui?.leadingIcon, item?.ui?.leadingIcon] })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: props.ui?.label })">
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: [props.ui?.label, item.ui?.label] })">
|
||||
<slot :item="item" :index="index" :open="open">{{ get(item, props.labelKey as string) }}</slot>
|
||||
</span>
|
||||
|
||||
<slot name="trailing" :item="item" :index="index" :open="open">
|
||||
<UIcon :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
||||
<UIcon :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.trailingIcon({ class: [props.ui?.trailingIcon, item.ui?.trailingIcon] })" />
|
||||
</slot>
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
|
||||
<AccordionContent v-if="item.content || !!slots.content || (item.slot && !!slots[item.slot as keyof AccordionSlots<T>]) || !!slots.body || (item.slot && !!slots[`${item.slot}-body` as keyof AccordionSlots<T>])" :class="ui.content({ class: props.ui?.content })">
|
||||
<AccordionContent v-if="item.content || !!slots.content || (item.slot && !!slots[item.slot as keyof AccordionSlots<T>]) || !!slots.body || (item.slot && !!slots[`${item.slot}-body` as keyof AccordionSlots<T>])" :class="ui.content({ class: [props.ui?.content, item.ui?.content] })">
|
||||
<slot :name="((item.slot || 'content') as keyof AccordionSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index" :open="open">
|
||||
<div :class="ui.body({ class: props.ui?.body })">
|
||||
<div :class="ui.body({ class: [props.ui?.body, item.ui?.body] })">
|
||||
<slot :name="((item.slot ? `${item.slot}-body`: 'body') as keyof AccordionSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index" :open="open">
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
|
||||
@@ -97,7 +97,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.alert || {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :data-orientation="orientation" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :data-orientation="orientation" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<slot name="leading">
|
||||
<UAvatar v-if="avatar" :size="((props.ui?.avatarSize || ui.avatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.avatar({ class: props.ui?.avatar })" />
|
||||
<UIcon v-else-if="icon" :name="icon" :class="ui.icon({ class: props.ui?.icon })" />
|
||||
|
||||
@@ -81,7 +81,7 @@ function onError() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })" :style="props.style">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })" :style="props.style">
|
||||
<component
|
||||
:is="ImageComponent"
|
||||
v-if="src && !error"
|
||||
|
||||
@@ -93,7 +93,7 @@ provide(avatarGroupInjectionKey, computed(() => ({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<UAvatar v-if="hiddenCount > 0" :text="`+${hiddenCount}`" :class="ui.base({ class: props.ui?.base })" />
|
||||
<component :is="avatar" v-for="(avatar, count) in visibleAvatars" :key="count" :class="ui.base({ class: props.ui?.base })" />
|
||||
</Primitive>
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface BadgeProps extends Omit<UseComponentIconsProps, 'loading' | 'lo
|
||||
* @defaultValue 'md'
|
||||
*/
|
||||
size?: Badge['variants']['size']
|
||||
/** Render the badge with equal padding on all sides. */
|
||||
square?: boolean
|
||||
class?: any
|
||||
ui?: Badge['slots']
|
||||
}
|
||||
@@ -50,7 +52,7 @@ import UAvatar from './Avatar.vue'
|
||||
const props = withDefaults(defineProps<BadgeProps>(), {
|
||||
as: 'span'
|
||||
})
|
||||
defineSlots<BadgeSlots>()
|
||||
const slots = defineSlots<BadgeSlots>()
|
||||
|
||||
const appConfig = useAppConfig() as Badge['AppConfig']
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<BadgeProps>(props)
|
||||
@@ -60,19 +62,20 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.badge || {})
|
||||
color: props.color,
|
||||
variant: props.variant,
|
||||
size: buttonGroupSize.value || props.size,
|
||||
square: props.square || (!slots.default && !props.label),
|
||||
buttonGroup: orientation.value
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.base({ class: [props.class, props.ui?.base] })">
|
||||
<Primitive :as="as" :class="ui.base({ class: [props.ui?.base, props.class] })">
|
||||
<slot name="leading">
|
||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||
<UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
|
||||
</slot>
|
||||
|
||||
<slot>
|
||||
<span v-if="label" :class="ui.label({ class: props.ui?.label })">
|
||||
<span v-if="label !== undefined && label !== null" :class="ui.label({ class: props.ui?.label })">
|
||||
{{ label }}
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface BreadcrumbItem extends Omit<LinkProps, 'raw' | 'custom'> {
|
||||
icon?: string
|
||||
avatar?: AvatarProps
|
||||
slot?: string
|
||||
class?: any
|
||||
ui?: Pick<Breadcrumb['slots'], 'item' | 'link' | 'linkLeadingIcon' | 'linkLeadingAvatar' | 'linkLabel' | 'separator' | 'separatorIcon'>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -81,19 +83,19 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.breadcrumb |
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" aria-label="breadcrumb" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" aria-label="breadcrumb" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<ol :class="ui.list({ class: props.ui?.list })">
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<li :class="ui.item({ class: props.ui?.item })">
|
||||
<li :class="ui.item({ class: [props.ui?.item, item.ui?.item] })">
|
||||
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
||||
<ULinkBase v-bind="slotProps" as="span" :aria-current="active && (index === items!.length - 1) ? 'page' : undefined" :class="ui.link({ class: [props.ui?.link, item.class], active: index === items!.length - 1, disabled: !!item.disabled, to: !!item.to })">
|
||||
<ULinkBase v-bind="slotProps" as="span" :aria-current="active && (index === items!.length - 1) ? 'page' : undefined" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: index === items!.length - 1, disabled: !!item.disabled, to: !!item.to })">
|
||||
<slot :name="((item.slot || 'item') as keyof BreadcrumbSlots<T>)" :item="item" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active: index === items!.length - 1 })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active: index === items!.length - 1 })" />
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon], active: index === items!.length - 1 })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: [props.ui?.linkLeadingAvatar, item.ui?.linkLeadingAvatar], active: index === items!.length - 1 })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>]" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>]" :class="ui.linkLabel({ class: [props.ui?.linkLabel, item.ui?.linkLabel] })">
|
||||
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
@@ -105,9 +107,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.breadcrumb |
|
||||
</ULink>
|
||||
</li>
|
||||
|
||||
<li v-if="index < items!.length - 1" role="presentation" aria-hidden="true" :class="ui.separator({ class: props.ui?.separator })">
|
||||
<li v-if="index < items!.length - 1" role="presentation" aria-hidden="true" :class="ui.separator({ class: [props.ui?.separator, item.ui?.separator] })">
|
||||
<slot name="separator">
|
||||
<UIcon :name="separatorIcon" :class="ui.separatorIcon({ class: props.ui?.separatorIcon })" />
|
||||
<UIcon :name="separatorIcon" :class="ui.separatorIcon({ class: [props.ui?.separatorIcon, item.ui?.separatorIcon] })" />
|
||||
</slot>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import theme from '#build/ui/button'
|
||||
import type { LinkProps } from './Link.vue'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import type { AvatarProps } from '../types'
|
||||
import type { LinkProps, AvatarProps } from '../types'
|
||||
import type { ComponentConfig } from '../types/utils'
|
||||
|
||||
type Button = ComponentConfig<typeof theme, AppConfig, 'button'>
|
||||
@@ -123,14 +122,13 @@ const ui = computed(() => tv({
|
||||
v-slot="{ active, ...slotProps }"
|
||||
:type="type"
|
||||
:disabled="disabled || isLoading"
|
||||
:class="ui.base({ class: [props.class, props.ui?.base] })"
|
||||
v-bind="omit(linkProps, ['type', 'disabled', 'onClick'])"
|
||||
custom
|
||||
>
|
||||
<ULinkBase
|
||||
v-bind="slotProps"
|
||||
:class="ui.base({
|
||||
class: [props.class, props.ui?.base],
|
||||
class: [props.ui?.base, props.class],
|
||||
active,
|
||||
...(active && activeVariant ? { variant: activeVariant } : {}),
|
||||
...(active && activeColor ? { color: activeColor } : {})
|
||||
@@ -143,7 +141,7 @@ const ui = computed(() => tv({
|
||||
</slot>
|
||||
|
||||
<slot>
|
||||
<span v-if="label" :class="ui.label({ class: props.ui?.label, active })">
|
||||
<span v-if="label !== undefined && label !== null" :class="ui.label({ class: props.ui?.label, active })">
|
||||
{{ label }}
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
@@ -157,7 +157,7 @@ const Calendar = computed(() => props.range ? RangeCalendar : SingleCalendar)
|
||||
:default-value="defaultValue"
|
||||
:locale="locale"
|
||||
:dir="dir"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
>
|
||||
<Calendar.Header :class="ui.header({ class: props.ui?.header })">
|
||||
<Calendar.Prev v-if="props.yearControls" :prev-page="(date: DateValue) => paginateYear(date, -1)" :aria-label="t('calendar.prevYear')" as-child>
|
||||
|
||||
@@ -43,7 +43,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.card || {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<div v-if="!!slots.header" :class="ui.header({ class: props.ui?.header })">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,13 @@ import type { ComponentConfig } from '../types/utils'
|
||||
|
||||
type Carousel = ComponentConfig<typeof theme, AppConfig, 'carousel'>
|
||||
|
||||
export type CarouselItem = AcceptableValue
|
||||
interface _CarouselItem {
|
||||
class?: any
|
||||
ui?: Pick<Carousel['slots'], 'item'>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type CarouselItem = _CarouselItem | AcceptableValue
|
||||
|
||||
export interface CarouselProps<T extends CarouselItem = CarouselItem> extends Omit<EmblaOptionsType, 'axis' | 'container' | 'slides' | 'direction'> {
|
||||
/**
|
||||
@@ -254,6 +260,10 @@ function onSelect(api: EmblaCarouselType) {
|
||||
emits('select', selectedIndex.value)
|
||||
}
|
||||
|
||||
function isCarouselItem(item: CarouselItem): item is _CarouselItem {
|
||||
return typeof item === 'object' && item !== null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!emblaApi.value) {
|
||||
return
|
||||
@@ -278,7 +288,7 @@ defineExpose({
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
tabindex="0"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
@keydown="onKeyDown"
|
||||
>
|
||||
<div ref="emblaRef" :class="ui.viewport({ class: props.ui?.viewport })">
|
||||
@@ -288,7 +298,7 @@ defineExpose({
|
||||
:key="index"
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:class="ui.item({ class: [props.ui?.item, isCarouselItem(item) && item.ui?.item, isCarouselItem(item) && item.class] })"
|
||||
>
|
||||
<slot :item="item" :index="index" />
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,7 @@ function onUpdate(value: any) {
|
||||
|
||||
<!-- eslint-disable vue/no-template-shadow -->
|
||||
<template>
|
||||
<Primitive :as="variant === 'list' ? as : Label" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="(!variant || variant === 'list') ? as : Label" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<div :class="ui.container({ class: props.ui?.container })">
|
||||
<CheckboxRoot
|
||||
:id="id"
|
||||
@@ -122,7 +122,7 @@ function onUpdate(value: any) {
|
||||
</div>
|
||||
|
||||
<div v-if="(label || !!slots.label) || (description || !!slots.description)" :class="ui.wrapper({ class: props.ui?.wrapper })">
|
||||
<component :is="variant === 'list' ? Label : 'p'" v-if="label || !!slots.label" :for="id" :class="ui.label({ class: props.ui?.label })">
|
||||
<component :is="(!variant || variant === 'list') ? Label : 'p'" v-if="label || !!slots.label" :for="id" :class="ui.label({ class: props.ui?.label })">
|
||||
<slot name="label" :label="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
|
||||
@@ -14,10 +14,12 @@ export type CheckboxGroupItem = {
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
value?: string
|
||||
class?: any
|
||||
ui?: Pick<CheckboxGroup['slots'], 'item'> & Omit<Required<CheckboxProps>['ui'], 'root'>
|
||||
[key: string]: any
|
||||
} | CheckboxGroupValue
|
||||
|
||||
export interface CheckboxGroupProps<T extends CheckboxGroupItem = CheckboxGroupItem> extends Pick<CheckboxGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'>, Pick<CheckboxProps, 'color' | 'variant' | 'indicator' | 'icon'> {
|
||||
export interface CheckboxGroupProps<T extends CheckboxGroupItem = CheckboxGroupItem> extends Pick<CheckboxGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'>, Pick<CheckboxProps, 'color' | 'indicator' | 'icon'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -44,6 +46,10 @@ export interface CheckboxGroupProps<T extends CheckboxGroupItem = CheckboxGroupI
|
||||
* @defaultValue 'md'
|
||||
*/
|
||||
size?: CheckboxGroup['variants']['size']
|
||||
/**
|
||||
* @defaultValue 'list'
|
||||
*/
|
||||
variant?: CheckboxGroup['variants']['variant']
|
||||
/**
|
||||
* The orientation the checkbox buttons are laid out.
|
||||
* @defaultValue 'vertical'
|
||||
@@ -74,6 +80,7 @@ import { useAppConfig } from '#imports'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { get, omit } from '../utils'
|
||||
import { tv } from '../utils/tv'
|
||||
import UCheckbox from './Checkbox.vue'
|
||||
|
||||
const props = withDefaults(defineProps<CheckboxGroupProps<T>>(), {
|
||||
valueKey: 'value',
|
||||
@@ -96,7 +103,9 @@ const id = _id.value ?? useId()
|
||||
const ui = computed(() => tv({ extend: theme, ...(appConfig.ui?.checkboxGroup || {}) })({
|
||||
size: size.value,
|
||||
required: props.required,
|
||||
orientation: props.orientation
|
||||
orientation: props.orientation,
|
||||
color: props.color,
|
||||
variant: props.variant
|
||||
}))
|
||||
|
||||
function normalizeItem(item: any) {
|
||||
@@ -152,7 +161,7 @@ function onUpdate(value: any) {
|
||||
v-bind="rootProps"
|
||||
:name="name"
|
||||
:disabled="disabled"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<fieldset :class="ui.fieldset({ class: props.ui?.fieldset })" v-bind="ariaAttrs">
|
||||
@@ -170,8 +179,8 @@ function onUpdate(value: any) {
|
||||
:size="size"
|
||||
:name="name"
|
||||
:disabled="item.disabled || disabled"
|
||||
:ui="props.ui ? omit(props.ui, ['root']) : undefined"
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:ui="{ ...(props.ui ? omit(props.ui, ['root']) : undefined), ...(item.ui || {}) }"
|
||||
:class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })"
|
||||
>
|
||||
<template v-for="(_, name) in proxySlots" #[name]>
|
||||
<slot :name="(name as keyof CheckboxGroupSlots<T>)" :item="item" />
|
||||
|
||||
@@ -74,7 +74,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.chip || {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<Slot v-bind="$attrs">
|
||||
<slot />
|
||||
</Slot>
|
||||
|
||||
@@ -46,7 +46,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.collapsible
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleRoot v-slot="{ open }" v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<CollapsibleRoot v-slot="{ open }" v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<CollapsibleTrigger v-if="!!slots.default" as-child>
|
||||
<slot :open="open" />
|
||||
</CollapsibleTrigger>
|
||||
|
||||
@@ -263,7 +263,7 @@ const trackThumbStyle = computed(() => ({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })" :data-disabled="disabled ? true : undefined">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })" :data-disabled="disabled ? true : undefined">
|
||||
<div :class="ui.picker({ class: props.ui?.picker })">
|
||||
<div
|
||||
ref="selectorRef"
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
||||
disabled?: boolean
|
||||
slot?: string
|
||||
onSelect?(e?: Event): void
|
||||
class?: any
|
||||
ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -249,7 +251,7 @@ const groups = computed(() => {
|
||||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<ListboxRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<ListboxRoot v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<ListboxFilter v-model="searchTerm" as-child>
|
||||
<UInput
|
||||
:placeholder="placeholder || t('commandPalette.placeholder')"
|
||||
@@ -293,42 +295,42 @@ const groups = computed(() => {
|
||||
@select="item.onSelect"
|
||||
>
|
||||
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
||||
<ULinkBase v-bind="slotProps" :class="ui.item({ class: props.ui?.item, active: active || item.active })">
|
||||
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class], active: active || item.active })">
|
||||
<slot :name="((item.slot || group.slot || 'item') as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, loading: true })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, active: active || item.active })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar, active: active || item.active })" />
|
||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon], loading: true })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon], active: active || item.active })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, item.ui?.itemLeadingAvatar], active: active || item.active })" />
|
||||
<UChip
|
||||
v-else-if="item.chip"
|
||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
:size="((item.ui?.itemLeadingChipSize || props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
inset
|
||||
standalone
|
||||
v-bind="item.chip"
|
||||
:class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip, active: active || item.active })"
|
||||
:class="ui.itemLeadingChip({ class: [props.ui?.itemLeadingChip, item.ui?.itemLeadingChip], active: active || item.active })"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>]" :class="ui.itemLabel({ class: props.ui?.itemLabel, active: active || item.active })">
|
||||
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>]" :class="ui.itemLabel({ class: [props.ui?.itemLabel, item.ui?.itemLabel], active: active || item.active })">
|
||||
<slot :name="((item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
|
||||
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: [props.ui?.itemLabelPrefix, item.ui?.itemLabelPrefix] })">{{ item.prefix }}</span>
|
||||
|
||||
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase, active: active || item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
|
||||
<span :class="ui.itemLabelBase({ class: [props.ui?.itemLabelBase, item.ui?.itemLabelBase], active: active || item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
|
||||
|
||||
<span :class="ui.itemLabelSuffix({ class: props.ui?.itemLabelSuffix, active: active || item.active })" v-html="item.suffixHtml || item.suffix" />
|
||||
<span :class="ui.itemLabelSuffix({ class: [props.ui?.itemLabelSuffix, item.ui?.itemLabelSuffix], active: active || item.active })" v-html="item.suffixHtml || item.suffix" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, item.ui?.itemTrailing] })">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||
<span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: props.ui?.itemTrailingKbds })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
<span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [props.ui?.itemTrailingKbds, item.ui?.itemTrailingKbds] })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((item.ui?.itemTrailingKbdsSize || props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
</span>
|
||||
<UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: props.ui?.itemTrailingHighlightedIcon })" />
|
||||
<UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: [props.ui?.itemTrailingHighlightedIcon, item.ui?.itemTrailingHighlightedIcon] })" />
|
||||
</slot>
|
||||
|
||||
<ListboxItemIndicator as-child>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })" />
|
||||
</ListboxItemIndicator>
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custo
|
||||
children?: ArrayOrNested<ContextMenuItem>
|
||||
onSelect?(e: Event): void
|
||||
onUpdateChecked?(checked: boolean): void
|
||||
class?: any
|
||||
ui?: Pick<ContextMenu['slots'], 'item' | 'label' | 'separator' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLabel' | 'itemLabelExternalIcon' | 'itemTrailing' | 'itemTrailingIcon' | 'itemTrailingKbds' | 'itemTrailingKbdsSize'>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
|
||||
@@ -77,29 +77,29 @@ const groups = computed<ContextMenuItem[][]>(() =>
|
||||
<DefineItemTemplate v-slot="{ item, active, index }">
|
||||
<slot :name="((item.slot || 'item') as keyof ContextMenuSlots<T>)" :item="item" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
|
||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: [uiOverride?.itemLeadingIcon, item.ui?.itemLeadingIcon], color: item?.color, loading: true })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [uiOverride?.itemLeadingIcon, item.ui?.itemLeadingIcon], color: item?.color, active })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [uiOverride?.itemLeadingAvatar, item.ui?.itemLeadingAvatar], active })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>]" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>]" :class="ui.itemLabel({ class: [uiOverride?.itemLabel, item.ui?.itemLabel], active })">
|
||||
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
|
||||
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, color: item?.color, active })" />
|
||||
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: [uiOverride?.itemLabelExternalIcon, item.ui?.itemLabelExternalIcon], color: item?.color, active })" />
|
||||
</span>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
||||
<span :class="ui.itemTrailing({ class: [uiOverride?.itemTrailing, item.ui?.itemTrailing] })">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UIcon v-if="item.children?.length" :name="childrenIcon" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
<UIcon v-if="item.children?.length" :name="childrenIcon" :class="ui.itemTrailingIcon({ class: [uiOverride?.itemTrailingIcon, item.ui?.itemTrailingIcon], color: item?.color, active })" />
|
||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [uiOverride?.itemTrailingKbds, item.ui?.itemTrailingKbds] })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((item.ui?.itemTrailingKbdsSize || props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
<ContextMenu.ItemIndicator as-child>
|
||||
<UIcon :name="checkedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color })" />
|
||||
<UIcon :name="checkedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [uiOverride?.itemTrailingIcon, item.ui?.itemTrailingIcon], color: item?.color })" />
|
||||
</ContextMenu.ItemIndicator>
|
||||
</span>
|
||||
</slot>
|
||||
@@ -111,17 +111,17 @@ const groups = computed<ContextMenuItem[][]>(() =>
|
||||
|
||||
<ContextMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
|
||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||
<ContextMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: uiOverride?.label })">
|
||||
<ContextMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
|
||||
<ReuseItemTemplate :item="item" :index="index" />
|
||||
</ContextMenu.Label>
|
||||
<ContextMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: uiOverride?.separator })" />
|
||||
<ContextMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
|
||||
<ContextMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
|
||||
<ContextMenu.SubTrigger
|
||||
as="button"
|
||||
type="button"
|
||||
:disabled="item.disabled"
|
||||
:text-value="get(item, props.labelKey as string)"
|
||||
:class="ui.item({ class: uiOverride?.item, color: item?.color })"
|
||||
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
|
||||
>
|
||||
<ReuseItemTemplate :item="item" :index="index" />
|
||||
</ContextMenu.SubTrigger>
|
||||
@@ -150,7 +150,7 @@ const groups = computed<ContextMenuItem[][]>(() =>
|
||||
:model-value="item.checked"
|
||||
:disabled="item.disabled"
|
||||
:text-value="get(item, props.labelKey as string)"
|
||||
:class="ui.item({ class: [uiOverride?.item, item.class], color: item?.color })"
|
||||
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
|
||||
@update:model-value="item.onUpdateChecked"
|
||||
@select="item.onSelect"
|
||||
>
|
||||
@@ -164,7 +164,7 @@ const groups = computed<ContextMenuItem[][]>(() =>
|
||||
@select="item.onSelect"
|
||||
>
|
||||
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" custom>
|
||||
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.class], active, color: item?.color })">
|
||||
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], active, color: item?.color })">
|
||||
<ReuseItemTemplate :item="item" :active="active" :index="index" />
|
||||
</ULinkBase>
|
||||
</ULink>
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cust
|
||||
children?: ArrayOrNested<DropdownMenuItem>
|
||||
onSelect?(e: Event): void
|
||||
onUpdateChecked?(checked: boolean): void
|
||||
class?: any
|
||||
ui?: Pick<DropdownMenu['slots'], 'item' | 'label' | 'separator' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLabel' | 'itemLabelExternalIcon' | 'itemTrailing' | 'itemTrailingIcon' | 'itemTrailingKbds' | 'itemTrailingKbdsSize'>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
|
||||
@@ -83,29 +83,29 @@ const groups = computed<DropdownMenuItem[][]>(() =>
|
||||
<DefineItemTemplate v-slot="{ item, active, index }">
|
||||
<slot :name="((item.slot || 'item') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
|
||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: [uiOverride?.itemLeadingIcon, item.ui?.itemLeadingIcon], color: item?.color, loading: true })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [uiOverride?.itemLeadingIcon, item.ui?.itemLeadingIcon], color: item?.color, active })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [uiOverride?.itemLeadingAvatar, item.ui?.itemLeadingAvatar], active })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof DropdownMenuContentSlots<T>]" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof DropdownMenuContentSlots<T>]" :class="ui.itemLabel({ class: [uiOverride?.itemLabel, item.ui?.itemLabel], active })">
|
||||
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
|
||||
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, color: item?.color, active })" />
|
||||
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: [uiOverride?.itemLabelExternalIcon, item.ui?.itemLabelExternalIcon], color: item?.color, active })" />
|
||||
</span>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
||||
<span :class="ui.itemTrailing({ class: [uiOverride?.itemTrailing, item.ui?.itemTrailing] })">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||
<UIcon v-if="item.children?.length" :name="childrenIcon" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
<UIcon v-if="item.children?.length" :name="childrenIcon" :class="ui.itemTrailingIcon({ class: [uiOverride?.itemTrailingIcon, item.ui?.itemTrailingIcon], color: item?.color, active })" />
|
||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [uiOverride?.itemTrailingKbds, item.ui?.itemTrailingKbds] })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((item.ui?.itemTrailingKbdsSize || props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
<DropdownMenu.ItemIndicator as-child>
|
||||
<UIcon :name="checkedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color })" />
|
||||
<UIcon :name="checkedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [uiOverride?.itemTrailingIcon, item.ui?.itemTrailingIcon], color: item?.color })" />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</span>
|
||||
</slot>
|
||||
@@ -117,17 +117,17 @@ const groups = computed<DropdownMenuItem[][]>(() =>
|
||||
|
||||
<DropdownMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
|
||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||
<DropdownMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: uiOverride?.label })">
|
||||
<DropdownMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
|
||||
<ReuseItemTemplate :item="item" :index="index" />
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: uiOverride?.separator })" />
|
||||
<DropdownMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
|
||||
<DropdownMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
|
||||
<DropdownMenu.SubTrigger
|
||||
as="button"
|
||||
type="button"
|
||||
:disabled="item.disabled"
|
||||
:text-value="get(item, props.labelKey as string)"
|
||||
:class="ui.item({ class: uiOverride?.item, color: item?.color })"
|
||||
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
|
||||
>
|
||||
<ReuseItemTemplate :item="item" :index="index" />
|
||||
</DropdownMenu.SubTrigger>
|
||||
@@ -158,7 +158,7 @@ const groups = computed<DropdownMenuItem[][]>(() =>
|
||||
:model-value="item.checked"
|
||||
:disabled="item.disabled"
|
||||
:text-value="get(item, props.labelKey as string)"
|
||||
:class="ui.item({ class: [uiOverride?.item, item.class], color: item?.color })"
|
||||
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
|
||||
@update:model-value="item.onUpdateChecked"
|
||||
@select="item.onSelect"
|
||||
>
|
||||
@@ -172,7 +172,7 @@ const groups = computed<DropdownMenuItem[][]>(() =>
|
||||
@select="item.onSelect"
|
||||
>
|
||||
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<DropdownMenuItem, 'type'>)" custom>
|
||||
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.class], color: item?.color, active })">
|
||||
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color, active })">
|
||||
<ReuseItemTemplate :item="item" :active="active" :index="index" />
|
||||
</ULinkBase>
|
||||
</ULink>
|
||||
|
||||
@@ -90,7 +90,7 @@ provide(formFieldInjectionKey, computed(() => ({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
|
||||
<div v-if="label || !!slots.label" :class="ui.labelWrapper({ class: props.ui?.labelWrapper })">
|
||||
<Label :for="id" :class="ui.label({ class: props.ui?.label })">
|
||||
@@ -115,16 +115,16 @@ provide(formFieldInjectionKey, computed(() => ({
|
||||
<div :class="[(label || !!slots.label || description || !!slots.description) && ui.container({ class: props.ui?.container })]">
|
||||
<slot :error="error" />
|
||||
|
||||
<p v-if="(typeof error === 'string' && error) || !!slots.error" :id="`${ariaId}-error`" :class="ui.error({ class: props.ui?.error })">
|
||||
<div v-if="(typeof error === 'string' && error) || !!slots.error" :id="`${ariaId}-error`" :class="ui.error({ class: props.ui?.error })">
|
||||
<slot name="error" :error="error">
|
||||
{{ error }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-else-if="help || !!slots.help" :class="ui.help({ class: props.ui?.help })">
|
||||
</div>
|
||||
<div v-else-if="help || !!slots.help" :class="ui.help({ class: props.ui?.help })">
|
||||
<slot name="help" :help="help">
|
||||
{{ help }}
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AppConfig } from '@nuxt/schema'
|
||||
import theme from '#build/ui/input'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import type { AvatarProps } from '../types'
|
||||
import type { ComponentConfig } from '../types/utils'
|
||||
import type { AcceptableValue, ComponentConfig } from '../types/utils'
|
||||
|
||||
type Input = ComponentConfig<typeof theme, AppConfig, 'input'>
|
||||
|
||||
@@ -42,8 +42,8 @@ export interface InputProps extends UseComponentIconsProps {
|
||||
ui?: Input['slots']
|
||||
}
|
||||
|
||||
export interface InputEmits {
|
||||
(e: 'update:modelValue', payload: string | number): void
|
||||
export interface InputEmits<T extends AcceptableValue = AcceptableValue> {
|
||||
(e: 'update:modelValue', payload: T): void
|
||||
(e: 'blur', event: FocusEvent): void
|
||||
(e: 'change', event: Event): void
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export interface InputSlots {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="T extends AcceptableValue">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
@@ -74,12 +74,13 @@ const props = withDefaults(defineProps<InputProps>(), {
|
||||
autocomplete: 'off',
|
||||
autofocusDelay: 0
|
||||
})
|
||||
const emits = defineEmits<InputEmits>()
|
||||
const emits = defineEmits<InputEmits<T>>()
|
||||
const slots = defineSlots<InputSlots>()
|
||||
|
||||
const [modelValue, modelModifiers] = defineModel<string | number | null>()
|
||||
const [modelValue, modelModifiers] = defineModel<T>()
|
||||
|
||||
const appConfig = useAppConfig() as Input['AppConfig']
|
||||
|
||||
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true })
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
|
||||
@@ -114,7 +115,7 @@ function updateInput(value: string | null) {
|
||||
value ||= null
|
||||
}
|
||||
|
||||
modelValue.value = value
|
||||
modelValue.value = value as T
|
||||
emitFormInput()
|
||||
}
|
||||
|
||||
@@ -163,7 +164,7 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<input
|
||||
:id="id"
|
||||
ref="inputRef"
|
||||
|
||||
@@ -24,6 +24,8 @@ interface _InputMenuItem {
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
disabled?: boolean
|
||||
onSelect?(e?: Event): void
|
||||
class?: any
|
||||
ui?: Pick<InputMenu['slots'], 'tagsItem' | 'tagsItemText' | 'tagsItemDelete' | 'tagsItemDeleteIcon' | 'label' | 'separator' | 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChip' | 'itemLeadingChipSize' | 'itemLabel' | 'itemTrailing' | 'itemTrailingIcon'>
|
||||
[key: string]: any
|
||||
}
|
||||
export type InputMenuItem = _InputMenuItem | AcceptableValue | boolean
|
||||
@@ -406,7 +408,7 @@ defineExpose({
|
||||
v-bind="rootProps"
|
||||
:name="name"
|
||||
:disabled="disabled"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
:as-child="!!multiple"
|
||||
ignore-filter
|
||||
@update:model-value="onUpdate"
|
||||
@@ -426,16 +428,16 @@ defineExpose({
|
||||
@focus="onFocus"
|
||||
@remove-tag="onRemoveTag"
|
||||
>
|
||||
<TagsInputItem v-for="(item, index) in tags" :key="index" :value="item" :class="ui.tagsItem({ class: props.ui?.tagsItem })">
|
||||
<TagsInputItemText :class="ui.tagsItemText({ class: props.ui?.tagsItemText })">
|
||||
<TagsInputItem v-for="(item, index) in tags" :key="index" :value="item" :class="ui.tagsItem({ class: [props.ui?.tagsItem, isInputItem(item) && item.ui?.tagsItem] })">
|
||||
<TagsInputItemText :class="ui.tagsItemText({ class: [props.ui?.tagsItemText, isInputItem(item) && item.ui?.tagsItemText] })">
|
||||
<slot name="tags-item-text" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ displayValue(item as T) }}
|
||||
</slot>
|
||||
</TagsInputItemText>
|
||||
|
||||
<TagsInputItemDelete :class="ui.tagsItemDelete({ class: props.ui?.tagsItemDelete })" :disabled="disabled">
|
||||
<TagsInputItemDelete :class="ui.tagsItemDelete({ class: [props.ui?.tagsItemDelete, isInputItem(item) && item.ui?.tagsItemDelete] })" :disabled="disabled">
|
||||
<slot name="tags-item-delete" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon :name="deleteIcon || appConfig.ui.icons.close" :class="ui.tagsItemDeleteIcon({ class: props.ui?.tagsItemDeleteIcon })" />
|
||||
<UIcon :name="deleteIcon || appConfig.ui.icons.close" :class="ui.tagsItemDeleteIcon({ class: [props.ui?.tagsItemDeleteIcon, isInputItem(item) && item.ui?.tagsItemDeleteIcon] })" />
|
||||
</slot>
|
||||
</TagsInputItemDelete>
|
||||
</TagsInputItem>
|
||||
@@ -493,44 +495,44 @@ defineExpose({
|
||||
|
||||
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||
<ComboboxLabel v-if="isInputItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
<ComboboxLabel v-if="isInputItem(item) && item.type === 'label'" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</ComboboxLabel>
|
||||
|
||||
<ComboboxSeparator v-else-if="isInputItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||
<ComboboxSeparator v-else-if="isInputItem(item) && item.type === 'separator'" :class="ui.separator({ class: [props.ui?.separator, item.ui?.separator, item.class] })" />
|
||||
|
||||
<ComboboxItem
|
||||
v-else
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:class="ui.item({ class: [props.ui?.item, isInputItem(item) && item.ui?.item, isInputItem(item) && item.class] })"
|
||||
:disabled="isInputItem(item) && item.disabled"
|
||||
:value="props.valueKey && isInputItem(item) ? get(item, props.valueKey as string) : item"
|
||||
@select="onSelect($event, item)"
|
||||
>
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon v-if="isInputItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<UAvatar v-else-if="isInputItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
<UIcon v-if="isInputItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon] })" />
|
||||
<UAvatar v-else-if="isInputItem(item) && item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, item.ui?.itemLeadingAvatar] })" />
|
||||
<UChip
|
||||
v-else-if="isInputItem(item) && item.chip"
|
||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
:size="((item.ui?.itemLeadingChipSize || props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
inset
|
||||
standalone
|
||||
v-bind="item.chip"
|
||||
:class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip })"
|
||||
:class="ui.itemLeadingChip({ class: [props.ui?.itemLeadingChip, item.ui?.itemLeadingChip] })"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||
<span :class="ui.itemLabel({ class: [props.ui?.itemLabel, isInputItem(item) && item.ui?.itemLabel] })">
|
||||
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ isInputItem(item) ? get(item, props.labelKey as string) : item }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, isInputItem(item) && item.ui?.itemTrailing] })">
|
||||
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
|
||||
|
||||
<ComboboxItemIndicator as-child>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, isInputItem(item) && item.ui?.itemTrailingIcon] })" />
|
||||
</ComboboxItemIndicator>
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue
|
||||
* @IconifyIcon
|
||||
*/
|
||||
incrementIcon?: string
|
||||
/** Disable the increment button. */
|
||||
incrementDisabled?: boolean
|
||||
/**
|
||||
* Configure the decrement button. The `color` and `size` are inherited.
|
||||
* @defaultValue { variant: 'link' }
|
||||
@@ -47,6 +49,8 @@ export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue
|
||||
* @IconifyIcon
|
||||
*/
|
||||
decrementIcon?: string
|
||||
/** Disable the decrement button. */
|
||||
decrementDisabled?: boolean
|
||||
autofocus?: boolean
|
||||
autofocusDelay?: number
|
||||
/**
|
||||
@@ -75,6 +79,7 @@ import { onMounted, ref, computed } from 'vue'
|
||||
import { NumberFieldRoot, NumberFieldInput, NumberFieldDecrement, NumberFieldIncrement, useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { useButtonGroup } from '../composables/useButtonGroup'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { tv } from '../utils/tv'
|
||||
@@ -83,26 +88,31 @@ import UButton from './Button.vue'
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<InputNumberProps>(), {
|
||||
orientation: 'horizontal'
|
||||
orientation: 'horizontal',
|
||||
disabledIncrement: false,
|
||||
disabledDecrement: false
|
||||
})
|
||||
const emits = defineEmits<InputNumberEmits>()
|
||||
defineSlots<InputNumberSlots>()
|
||||
|
||||
const { t, code: codeLocale } = useLocale()
|
||||
const appConfig = useAppConfig() as InputNumber['AppConfig']
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange'), emits)
|
||||
|
||||
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
|
||||
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputNumberProps>(props)
|
||||
|
||||
const { t, code: codeLocale } = useLocale()
|
||||
const locale = computed(() => props.locale || codeLocale.value)
|
||||
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
|
||||
|
||||
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputNumber || {}) })({
|
||||
color: color.value,
|
||||
variant: props.variant,
|
||||
size: size.value,
|
||||
size: inputSize.value,
|
||||
highlight: highlight.value,
|
||||
orientation: props.orientation
|
||||
orientation: props.orientation,
|
||||
buttonGroup: orientation.value
|
||||
}))
|
||||
|
||||
const incrementIcon = computed(() => props.incrementIcon || (props.orientation === 'horizontal' ? appConfig.ui.icons.plus : appConfig.ui.icons.chevronUp))
|
||||
@@ -145,7 +155,7 @@ defineExpose({
|
||||
<NumberFieldRoot
|
||||
v-bind="rootProps"
|
||||
:id="id"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
:name="name"
|
||||
:disabled="disabled"
|
||||
:locale="locale"
|
||||
@@ -162,7 +172,7 @@ defineExpose({
|
||||
/>
|
||||
|
||||
<div :class="ui.increment({ class: props.ui?.increment })">
|
||||
<NumberFieldIncrement as-child :disabled="disabled">
|
||||
<NumberFieldIncrement as-child :disabled="disabled || incrementDisabled">
|
||||
<slot name="increment">
|
||||
<UButton
|
||||
:icon="incrementIcon"
|
||||
@@ -177,7 +187,7 @@ defineExpose({
|
||||
</div>
|
||||
|
||||
<div :class="ui.decrement({ class: props.ui?.decrement })">
|
||||
<NumberFieldDecrement as-child :disabled="disabled">
|
||||
<NumberFieldDecrement as-child :disabled="disabled || decrementDisabled">
|
||||
<slot name="decrement">
|
||||
<UButton
|
||||
:icon="decrementIcon"
|
||||
|
||||
@@ -89,11 +89,12 @@ export interface LinkSlots {
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { isEqual, diff } from 'ohash/utils'
|
||||
import { isEqual } from 'ohash/utils'
|
||||
import { useForwardProps } from 'reka-ui'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { useRoute, useAppConfig } from '#imports'
|
||||
import { tv } from '../utils/tv'
|
||||
import { isPartiallyEqual } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
@@ -111,7 +112,7 @@ defineSlots<LinkSlots>()
|
||||
const route = useRoute()
|
||||
const appConfig = useAppConfig() as Link['AppConfig']
|
||||
|
||||
const nuxtLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'raw', 'class'))
|
||||
const nuxtLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'to', 'href', 'raw', 'custom', 'class'))
|
||||
|
||||
const ui = computed(() => tv({
|
||||
extend: tv(theme),
|
||||
@@ -125,19 +126,7 @@ const ui = computed(() => tv({
|
||||
}, appConfig.ui?.link || {})
|
||||
}))
|
||||
|
||||
function isPartiallyEqual(item1: any, item2: any) {
|
||||
const diffedKeys = diff(item1, item2).reduce((filtered, q) => {
|
||||
if (q.type === 'added') {
|
||||
filtered.add(q.key)
|
||||
}
|
||||
return filtered
|
||||
}, new Set<string>())
|
||||
|
||||
const item1Filtered = Object.fromEntries(Object.entries(item1).filter(([key]) => !diffedKeys.has(key)))
|
||||
const item2Filtered = Object.fromEntries(Object.entries(item2).filter(([key]) => !diffedKeys.has(key)))
|
||||
|
||||
return isEqual(item1Filtered, item2Filtered)
|
||||
}
|
||||
const to = computed(() => props.to ?? props.href)
|
||||
|
||||
function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
|
||||
if (props.active !== undefined) {
|
||||
@@ -177,7 +166,7 @@ function resolveLinkClass({ route, isActive, isExactActive }: any) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink v-slot="{ href, navigate, route: linkRoute, rel, target, isExternal, isActive, isExactActive }" v-bind="nuxtLinkProps" custom>
|
||||
<NuxtLink v-slot="{ href, navigate, route: linkRoute, rel, target, isExternal, isActive, isExactActive }" v-bind="nuxtLinkProps" :to="to" custom>
|
||||
<template v-if="custom">
|
||||
<slot
|
||||
v-bind="{
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, NavigationMenuContentEmits, CollapsibleRootProps } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import theme from '#build/ui/navigation-menu'
|
||||
import type { AvatarProps, BadgeProps, LinkProps } from '../types'
|
||||
import type { AvatarProps, BadgeProps, LinkProps, TooltipProps } from '../types'
|
||||
import type { ArrayOrNested, DynamicSlots, MergeTypes, NestedItem, EmitsToProps, ComponentConfig } from '../types/utils'
|
||||
|
||||
type NavigationMenu = ComponentConfig<typeof theme, AppConfig, 'navigationMenu'>
|
||||
|
||||
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type'> {
|
||||
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type' | 'ui'> {
|
||||
/** Description is only used when `orientation` is `horizontal`. */
|
||||
description?: string
|
||||
[key: string]: any
|
||||
@@ -26,6 +26,12 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
||||
* `{ size: 'sm', color: 'neutral', variant: 'outline' }`{lang="ts-type"}
|
||||
*/
|
||||
badge?: string | number | BadgeProps
|
||||
/**
|
||||
* Display a tooltip on the item.
|
||||
* Only works when `type` is `link`.
|
||||
* `{ content: { side: 'right' } }`{lang="ts-type"}
|
||||
*/
|
||||
tooltip?: TooltipProps
|
||||
/**
|
||||
* @IconifyIcon
|
||||
*/
|
||||
@@ -37,9 +43,21 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
||||
*/
|
||||
type?: 'label' | 'link'
|
||||
slot?: string
|
||||
/**
|
||||
* The value of the item. Avoid using `index` as the value to prevent conflicts in horizontal orientation with Reka UI.
|
||||
* @defaultValue `item-${index}`
|
||||
*/
|
||||
value?: string
|
||||
/**
|
||||
* Make the item collapsible.
|
||||
* Only works when `orientation` is `vertical`.
|
||||
* @defaultValue true
|
||||
*/
|
||||
collapsible?: boolean
|
||||
children?: NavigationMenuChildItem[]
|
||||
onSelect?(e: Event): void
|
||||
class?: any
|
||||
ui?: Pick<NavigationMenu['slots'], 'item' | 'linkLeadingAvatarSize' | 'linkLeadingAvatar' | 'linkLeadingIcon' | 'linkLabel' | 'linkLabelExternalIcon' | 'linkTrailing' | 'linkTrailingBadgeSize' | 'linkTrailingBadge' | 'linkTrailingIcon' | 'label' | 'link' | 'content' | 'childList' | 'childItem' | 'childLink' | 'childLinkIcon' | 'childLinkWrapper' | 'childLinkLabel' | 'childLinkLabelExternalIcon' | 'childLinkDescription'>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -143,6 +161,7 @@ import UAvatar from './Avatar.vue'
|
||||
import UIcon from './Icon.vue'
|
||||
import UBadge from './Badge.vue'
|
||||
import UCollapsible from './Collapsible.vue'
|
||||
import UTooltip from './Tooltip.vue'
|
||||
|
||||
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
|
||||
orientation: 'horizontal',
|
||||
@@ -203,34 +222,34 @@ const lists = computed<NavigationMenuItem[][]>(() =>
|
||||
<DefineLinkTemplate v-slot="{ item, active, index }">
|
||||
<slot :name="((item.slot || 'item') as keyof NavigationMenuSlots<T>)" :item="item" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading` : 'item-leading') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
|
||||
<UAvatar v-if="item.avatar" :size="((item.ui?.linkLeadingAvatarSize || props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: [props.ui?.linkLeadingAvatar, item.ui?.linkLeadingAvatar], active, disabled: !!item.disabled })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon], active, disabled: !!item.disabled })" />
|
||||
</slot>
|
||||
|
||||
<span
|
||||
v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>])"
|
||||
:class="ui.linkLabel({ class: props.ui?.linkLabel })"
|
||||
:class="ui.linkLabel({ class: [props.ui?.linkLabel, item.ui?.linkLabel] })"
|
||||
>
|
||||
<slot :name="((item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
|
||||
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
|
||||
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: [props.ui?.linkLabelExternalIcon, item.ui?.linkLabelExternalIcon], active })" />
|
||||
</span>
|
||||
|
||||
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
||||
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" :class="ui.linkTrailing({ class: [props.ui?.linkTrailing, item.ui?.linkTrailing] })">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UBadge
|
||||
v-if="item.badge"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:size="((props.ui?.linkTrailingBadgeSize || ui.linkTrailingBadgeSize()) as BadgeProps['size'])"
|
||||
:size="((item.ui?.linkTrailingBadgeSize || props.ui?.linkTrailingBadgeSize || ui.linkTrailingBadgeSize()) as BadgeProps['size'])"
|
||||
v-bind="(typeof item.badge === 'string' || typeof item.badge === 'number') ? { label: item.badge } : item.badge"
|
||||
:class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
|
||||
:class="ui.linkTrailingBadge({ class: [props.ui?.linkTrailingBadge, item.ui?.linkTrailingBadge] })"
|
||||
/>
|
||||
|
||||
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
||||
<UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
||||
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length && item.collapsible !== false)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon], active })" />
|
||||
<UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon], active })" />
|
||||
</slot>
|
||||
</span>
|
||||
</slot>
|
||||
@@ -238,17 +257,18 @@ const lists = computed<NavigationMenuItem[][]>(() =>
|
||||
|
||||
<DefineItemTemplate v-slot="{ item, index, level = 0 }">
|
||||
<component
|
||||
:is="(orientation === 'vertical' && item.children?.length && !collapsed) ? UCollapsible : NavigationMenuItem"
|
||||
:is="(orientation === 'vertical' && item.children?.length) ? UCollapsible : NavigationMenuItem"
|
||||
as="li"
|
||||
:value="item.value || String(index)"
|
||||
:value="item.value || `item-${index}`"
|
||||
:default-open="item.defaultOpen"
|
||||
:unmount-on-hide="(orientation === 'vertical' && item.children?.length && !collapsed) ? unmountOnHide : undefined"
|
||||
:disabled="(orientation === 'vertical' && item.children?.length) ? item.collapsible === false : undefined"
|
||||
:unmount-on-hide="(orientation === 'vertical' && item.children?.length) ? unmountOnHide : undefined"
|
||||
:open="item.open"
|
||||
>
|
||||
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })">
|
||||
<ReuseLinkTemplate :item="item" :index="index" />
|
||||
</div>
|
||||
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && !collapsed) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
|
||||
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && item.collapsible !== false) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
|
||||
<component
|
||||
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : NavigationMenuLink"
|
||||
as-child
|
||||
@@ -256,27 +276,32 @@ const lists = computed<NavigationMenuItem[][]>(() =>
|
||||
:disabled="item.disabled"
|
||||
@select="item.onSelect"
|
||||
>
|
||||
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
|
||||
<UTooltip v-if="!!item.tooltip" :content="{ side: 'right' }" v-bind="item.tooltip">
|
||||
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
|
||||
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
|
||||
</ULinkBase>
|
||||
</UTooltip>
|
||||
<ULinkBase v-else v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
|
||||
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
|
||||
</ULinkBase>
|
||||
</component>
|
||||
|
||||
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
|
||||
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])" v-bind="contentProps" :class="ui.content({ class: [props.ui?.content, item.ui?.content] })">
|
||||
<slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<ul :class="ui.childList({ class: props.ui?.childList })">
|
||||
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
|
||||
<ul :class="ui.childList({ class: [props.ui?.childList, item.ui?.childList] })">
|
||||
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: [props.ui?.childItem, item.ui?.childItem] })">
|
||||
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
|
||||
<NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
|
||||
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, childItem.class], active: childActive })">
|
||||
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: props.ui?.childLinkIcon, active: childActive })" />
|
||||
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, item.ui?.childLink, childItem.class], active: childActive })">
|
||||
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: [props.ui?.childLinkIcon, item.ui?.childLinkIcon], active: childActive })" />
|
||||
|
||||
<div :class="ui.childLinkWrapper({ class: props.ui?.childLinkWrapper })">
|
||||
<p :class="ui.childLinkLabel({ class: props.ui?.childLinkLabel, active: childActive })">
|
||||
<div :class="ui.childLinkWrapper({ class: [props.ui?.childLinkWrapper, item.ui?.childLinkWrapper] })">
|
||||
<p :class="ui.childLinkLabel({ class: [props.ui?.childLinkLabel, item.ui?.childLinkLabel], active: childActive })">
|
||||
{{ get(childItem, props.labelKey as string) }}
|
||||
|
||||
<UIcon v-if="childItem.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: props.ui?.childLinkLabelExternalIcon, active: childActive })" />
|
||||
<UIcon v-if="childItem.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: [props.ui?.childLinkLabelExternalIcon, item.ui?.childLinkLabelExternalIcon], active: childActive })" />
|
||||
</p>
|
||||
<p v-if="childItem.description" :class="ui.childLinkDescription({ class: props.ui?.childLinkDescription, active: childActive })">
|
||||
<p v-if="childItem.description" :class="ui.childLinkDescription({ class: [props.ui?.childLinkDescription, item.ui?.childLinkDescription], active: childActive })">
|
||||
{{ childItem.description }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -289,7 +314,7 @@ const lists = computed<NavigationMenuItem[][]>(() =>
|
||||
</NavigationMenuContent>
|
||||
</ULink>
|
||||
|
||||
<template v-if="orientation === 'vertical' && item.children?.length && !collapsed" #content>
|
||||
<template v-if="orientation === 'vertical' && item.children?.length " #content>
|
||||
<ul :class="ui.childList({ class: props.ui?.childList })">
|
||||
<ReuseItemTemplate
|
||||
v-for="(childItem, childIndex) in item.children"
|
||||
@@ -297,19 +322,19 @@ const lists = computed<NavigationMenuItem[][]>(() =>
|
||||
:item="childItem"
|
||||
:index="childIndex"
|
||||
:level="level + 1"
|
||||
:class="ui.childItem({ class: props.ui?.childItem })"
|
||||
:class="ui.childItem({ class: [props.ui?.childItem, childItem.ui?.childItem] })"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
</component>
|
||||
</DefineItemTemplate>
|
||||
|
||||
<NavigationMenuRoot v-bind="rootProps" :data-collapsed="collapsed" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<NavigationMenuRoot v-bind="rootProps" :data-collapsed="collapsed" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<slot name="list-leading" />
|
||||
|
||||
<template v-for="(list, listIndex) in lists" :key="`list-${listIndex}`">
|
||||
<NavigationMenuList :class="ui.list({ class: props.ui?.list })">
|
||||
<ReuseItemTemplate v-for="(item, index) in list" :key="`list-${listIndex}-${index}`" :item="item" :index="index" :class="ui.item({ class: props.ui?.item })" />
|
||||
<ReuseItemTemplate v-for="(item, index) in list" :key="`list-${listIndex}-${index}`" :item="item" :index="index" :class="ui.item({ class: [props.ui?.item, item.ui?.item] })" />
|
||||
</NavigationMenuList>
|
||||
|
||||
<div v-if="orientation === 'vertical' && listIndex < lists.length - 1" :class="ui.separator({ class: props.ui?.separator })" />
|
||||
|
||||
@@ -140,7 +140,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.pagination |
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationRoot v-slot="{ page, pageCount }" v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<PaginationRoot v-slot="{ page, pageCount }" v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<PaginationList v-slot="{ items }" :class="ui.list({ class: props.ui?.list })">
|
||||
<PaginationFirst v-if="showControls || !!slots.first" as-child :class="ui.first({ class: props.ui?.first })">
|
||||
<slot name="first">
|
||||
|
||||
@@ -113,7 +113,7 @@ defineExpose({
|
||||
v-bind="{ ...rootProps, ...ariaAttrs }"
|
||||
:id="id"
|
||||
:name="name"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
@update:model-value="emitFormInput()"
|
||||
@complete="onComplete"
|
||||
>
|
||||
|
||||
@@ -167,7 +167,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.progress ||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<div v-if="!isIndeterminate && (status || !!slots.status)" :class="ui.status({ class: props.ui?.status })" :style="statusStyle">
|
||||
<slot name="status" :percent="percent">
|
||||
{{ percent }}%
|
||||
|
||||
@@ -12,6 +12,8 @@ export type RadioGroupItem = {
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
value?: RadioGroupValue
|
||||
class?: any
|
||||
ui?: Pick<RadioGroup['slots'], 'item' | 'container' | 'base' | 'indicator' | 'wrapper' | 'label' | 'description'>
|
||||
[key: string]: any
|
||||
} | RadioGroupValue
|
||||
|
||||
@@ -166,7 +168,7 @@ function onUpdate(value: any) {
|
||||
v-bind="rootProps"
|
||||
:name="name"
|
||||
:disabled="disabled"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<fieldset :class="ui.fieldset({ class: props.ui?.fieldset })" v-bind="ariaAttrs">
|
||||
@@ -176,25 +178,25 @@ function onUpdate(value: any) {
|
||||
</slot>
|
||||
</legend>
|
||||
|
||||
<component :is="variant === 'list' ? 'div' : Label" v-for="item in normalizedItems" :key="item.value" :class="ui.item({ class: props.ui?.item })">
|
||||
<div :class="ui.container({ class: props.ui?.container })">
|
||||
<component :is="(!variant || variant === 'list') ? 'div' : Label" v-for="item in normalizedItems" :key="item.value" :class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })">
|
||||
<div :class="ui.container({ class: [props.ui?.container, item.ui?.container] })">
|
||||
<RadioGroupItem
|
||||
:id="item.id"
|
||||
:value="item.value"
|
||||
:disabled="item.disabled"
|
||||
:class="ui.base({ class: props.ui?.base, disabled: item.disabled })"
|
||||
:class="ui.base({ class: [props.ui?.base, item.ui?.base], disabled: item.disabled })"
|
||||
>
|
||||
<RadioGroupIndicator :class="ui.indicator({ class: props.ui?.indicator })" />
|
||||
<RadioGroupIndicator :class="ui.indicator({ class: [props.ui?.indicator, item.ui?.indicator] })" />
|
||||
</RadioGroupItem>
|
||||
</div>
|
||||
|
||||
<div v-if="(item.label || !!slots.label) || (item.description || !!slots.description)" :class="ui.wrapper({ class: props.ui?.wrapper })">
|
||||
<component :is="variant === 'list' ? Label : 'p'" v-if="item.label || !!slots.label" :for="item.id" :class="ui.label({ class: props.ui?.label })">
|
||||
<div v-if="(item.label || !!slots.label) || (item.description || !!slots.description)" :class="ui.wrapper({ class: [props.ui?.wrapper, item.ui?.wrapper] })">
|
||||
<component :is="(!variant || variant === 'list') ? Label : 'p'" v-if="item.label || !!slots.label" :for="item.id" :class="ui.label({ class: [props.ui?.label, item.ui?.label] })">
|
||||
<slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</component>
|
||||
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
|
||||
<p v-if="item.description || !!slots.description" :class="ui.description({ class: [props.ui?.description, item.ui?.description] })">
|
||||
<slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)">
|
||||
{{ item.description }}
|
||||
</slot>
|
||||
|
||||
@@ -21,9 +21,11 @@ interface SelectItemBase {
|
||||
* @defaultValue 'item'
|
||||
*/
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
value?: string | number
|
||||
value?: AcceptableValue | boolean
|
||||
disabled?: boolean
|
||||
onSelect?(e?: Event): void
|
||||
class?: any
|
||||
ui?: Pick<Select['slots'], 'label' | 'separator' | 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemTrailing' | 'itemTrailingIcon'>
|
||||
[key: string]: any
|
||||
}
|
||||
export type SelectItem = SelectItemBase | AcceptableValue | boolean
|
||||
@@ -238,7 +240,7 @@ function isSelectItem(item: SelectItem): item is SelectItemBase {
|
||||
@update:model-value="onUpdate"
|
||||
@update:open="onUpdateOpen"
|
||||
>
|
||||
<SelectTrigger :id="id" :class="ui.base({ class: [props.class, props.ui?.base] })" v-bind="{ ...$attrs, ...ariaAttrs }">
|
||||
<SelectTrigger :id="id" :class="ui.base({ class: [props.ui?.base, props.class] })" v-bind="{ ...$attrs, ...ariaAttrs }">
|
||||
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
||||
<slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||
@@ -271,44 +273,44 @@ function isSelectItem(item: SelectItem): item is SelectItemBase {
|
||||
<SelectViewport :class="ui.viewport({ class: props.ui?.viewport })">
|
||||
<SelectGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||
<SelectLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
<SelectLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</SelectLabel>
|
||||
|
||||
<SelectSeparator v-else-if="isSelectItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||
<SelectSeparator v-else-if="isSelectItem(item) && item.type === 'separator'" :class="ui.separator({ class: [props.ui?.separator, item.ui?.separator, item.class] })" />
|
||||
|
||||
<SelectItem
|
||||
v-else
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:class="ui.item({ class: [props.ui?.item, isSelectItem(item) && item.ui?.item, isSelectItem(item) && item.class] })"
|
||||
:disabled="isSelectItem(item) && item.disabled"
|
||||
:value="isSelectItem(item) ? get(item, props.valueKey as string) : item"
|
||||
@select="isSelectItem(item) && item.onSelect?.($event)"
|
||||
>
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon] })" />
|
||||
<UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, item.ui?.itemLeadingAvatar] })" />
|
||||
<UChip
|
||||
v-else-if="isSelectItem(item) && item.chip"
|
||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
:size="((item.ui?.itemLeadingChipSize || props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
inset
|
||||
standalone
|
||||
v-bind="item.chip"
|
||||
:class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip })"
|
||||
:class="ui.itemLeadingChip({ class: [props.ui?.itemLeadingChip, item.ui?.itemLeadingChip] })"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<SelectItemText :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||
<SelectItemText :class="ui.itemLabel({ class: [props.ui?.itemLabel, isSelectItem(item) && item.ui?.itemLabel] })">
|
||||
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
|
||||
</slot>
|
||||
</SelectItemText>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, isSelectItem(item) && item.ui?.itemTrailing] })">
|
||||
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
|
||||
|
||||
<SelectItemIndicator as-child>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, isSelectItem(item) && item.ui?.itemTrailingIcon] })" />
|
||||
</SelectItemIndicator>
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
@@ -23,6 +23,8 @@ interface _SelectMenuItem {
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
disabled?: boolean
|
||||
onSelect?(e?: Event): void
|
||||
class?: any
|
||||
ui?: Pick<SelectMenu['slots'], 'label' | 'separator' | 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemTrailing' | 'itemTrailingIcon'>
|
||||
[key: string]: any
|
||||
}
|
||||
export type SelectMenuItem = _SelectMenuItem | AcceptableValue | boolean
|
||||
@@ -373,7 +375,7 @@ function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
|
||||
@update:open="onUpdateOpen"
|
||||
>
|
||||
<ComboboxAnchor as-child>
|
||||
<ComboboxTrigger :class="ui.base({ class: [props.class, props.ui?.base] })" tabindex="0">
|
||||
<ComboboxTrigger :class="ui.base({ class: [props.ui?.base, props.class] })" tabindex="0">
|
||||
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
||||
<slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||
@@ -420,44 +422,44 @@ function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
|
||||
|
||||
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||
<ComboboxLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
<ComboboxLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</ComboboxLabel>
|
||||
|
||||
<ComboboxSeparator v-else-if="isSelectItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||
<ComboboxSeparator v-else-if="isSelectItem(item) && item.type === 'separator'" :class="ui.separator({ class: [props.ui?.separator, item.ui?.separator, item.class] })" />
|
||||
|
||||
<ComboboxItem
|
||||
v-else
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:class="ui.item({ class: [props.ui?.item, isSelectItem(item) && item.ui?.item, isSelectItem(item) && item.class] })"
|
||||
:disabled="isSelectItem(item) && item.disabled"
|
||||
:value="props.valueKey && isSelectItem(item) ? get(item, props.valueKey as string) : item"
|
||||
@select="onSelect($event, item)"
|
||||
>
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon] })" />
|
||||
<UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, item.ui?.itemLeadingAvatar] })" />
|
||||
<UChip
|
||||
v-else-if="isSelectItem(item) && item.chip"
|
||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
inset
|
||||
standalone
|
||||
v-bind="item.chip"
|
||||
:class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip })"
|
||||
:class="ui.itemLeadingChip({ class: [props.ui?.itemLeadingChip, item.ui?.itemLeadingChip] })"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||
<span :class="ui.itemLabel({ class: [props.ui?.itemLabel, isSelectItem(item) && item.ui?.itemLabel] })">
|
||||
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, isSelectItem(item) && item.ui?.itemTrailing] })">
|
||||
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
|
||||
|
||||
<ComboboxItemIndicator as-child>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, isSelectItem(item) && item.ui?.itemTrailingIcon] })" />
|
||||
</ComboboxItemIndicator>
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
@@ -75,7 +75,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.separator ||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Separator v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Separator v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<div :class="ui.border({ class: props.ui?.border })" />
|
||||
|
||||
<template v-if="label || icon || avatar || !!slots.default">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { SliderRootProps } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import theme from '#build/ui/slider'
|
||||
import type { TooltipProps } from '../types'
|
||||
import type { ComponentConfig } from '../types/utils'
|
||||
|
||||
type Slider = ComponentConfig<typeof theme, AppConfig, 'slider'>
|
||||
@@ -25,25 +26,32 @@ export interface SliderProps extends Pick<SliderRootProps, 'name' | 'disabled' |
|
||||
* @defaultValue 'horizontal'
|
||||
*/
|
||||
orientation?: SliderRootProps['orientation']
|
||||
/**
|
||||
* Display a tooltip around the slider thumbs with the current value.
|
||||
* `{ disableClosingTrigger: true }`{lang="ts-type"}
|
||||
* @defaultValue false
|
||||
*/
|
||||
tooltip?: boolean | TooltipProps
|
||||
/** The value of the slider when initially rendered. Use when you do not need to control the state of the slider. */
|
||||
defaultValue?: number | number[]
|
||||
class?: any
|
||||
ui?: Slider['slots']
|
||||
}
|
||||
|
||||
export interface SliderEmits {
|
||||
(e: 'update:modelValue', payload: number | number[]): void
|
||||
export interface SliderEmits<T extends number | number[] = number | number[]> {
|
||||
(e: 'update:modelValue', payload: T): void
|
||||
(e: 'change', payload: Event): void
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="T extends number | number[]">
|
||||
import { computed } from 'vue'
|
||||
import { SliderRoot, SliderRange, SliderTrack, SliderThumb, useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { tv } from '../utils/tv'
|
||||
import UTooltip from './Tooltip.vue'
|
||||
|
||||
const props = withDefaults(defineProps<SliderProps>(), {
|
||||
min: 0,
|
||||
@@ -51,9 +59,9 @@ const props = withDefaults(defineProps<SliderProps>(), {
|
||||
step: 1,
|
||||
orientation: 'horizontal'
|
||||
})
|
||||
const emits = defineEmits<SliderEmits>()
|
||||
const emits = defineEmits<SliderEmits<T>>()
|
||||
|
||||
const modelValue = defineModel<number | number[]>()
|
||||
const modelValue = defineModel<T>()
|
||||
|
||||
const appConfig = useAppConfig() as Slider['AppConfig']
|
||||
|
||||
@@ -73,14 +81,14 @@ const sliderValue = computed({
|
||||
if (typeof modelValue.value === 'number') {
|
||||
return [modelValue.value]
|
||||
}
|
||||
return modelValue.value ?? defaultSliderValue.value
|
||||
return (modelValue.value as number[]) ?? defaultSliderValue.value
|
||||
},
|
||||
set(value) {
|
||||
modelValue.value = value?.length !== 1 ? value : value[0]
|
||||
modelValue.value = (value?.length !== 1 ? value : value[0]) as T
|
||||
}
|
||||
})
|
||||
|
||||
const thumbsCount = computed(() => sliderValue.value?.length ?? 1)
|
||||
const thumbs = computed(() => sliderValue.value?.length ?? 1)
|
||||
|
||||
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slider || {}) })({
|
||||
disabled: disabled.value,
|
||||
@@ -104,7 +112,7 @@ function onChange(value: any) {
|
||||
v-model="sliderValue"
|
||||
:name="name"
|
||||
:disabled="disabled"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
:default-value="defaultSliderValue"
|
||||
@update:model-value="emitFormInput()"
|
||||
@value-commit="onChange"
|
||||
@@ -113,6 +121,16 @@ function onChange(value: any) {
|
||||
<SliderRange :class="ui.range({ class: props.ui?.range })" />
|
||||
</SliderTrack>
|
||||
|
||||
<SliderThumb v-for="count in thumbsCount" :key="count" :class="ui.thumb({ class: props.ui?.thumb })" />
|
||||
<template v-for="thumb in thumbs" :key="thumb">
|
||||
<UTooltip
|
||||
v-if="!!tooltip"
|
||||
:text="thumbs > 1 ? String(sliderValue?.[thumb - 1]) : String(sliderValue)"
|
||||
disable-closing-trigger
|
||||
v-bind="(typeof tooltip === 'object' ? tooltip : {})"
|
||||
>
|
||||
<SliderThumb :class="ui.thumb({ class: props.ui?.thumb })" />
|
||||
</UTooltip>
|
||||
<SliderThumb v-else :class="ui.thumb({ class: props.ui?.thumb })" />
|
||||
</template>
|
||||
</SliderRoot>
|
||||
</template>
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface StepperItem {
|
||||
icon?: string
|
||||
content?: string
|
||||
disabled?: boolean
|
||||
class?: any
|
||||
ui?: Pick<Stepper['slots'], 'item' | 'container' | 'trigger' | 'indicator' | 'icon' | 'separator' | 'wrapper' | 'title' | 'description'>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -129,20 +131,20 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<StepperRoot v-bind="rootProps" v-model="currentStepIndex" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<StepperRoot v-bind="rootProps" v-model="currentStepIndex" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<div :class="ui.header({ class: props.ui?.header })">
|
||||
<StepperItem
|
||||
v-for="(item, count) in items"
|
||||
:key="item.value ?? count"
|
||||
:step="count"
|
||||
:disabled="item.disabled || props.disabled"
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })"
|
||||
>
|
||||
<div :class="ui.container({ class: props.ui?.container })">
|
||||
<StepperTrigger :class="ui.trigger({ class: props.ui?.trigger })">
|
||||
<StepperIndicator :class="ui.indicator({ class: props.ui?.indicator })">
|
||||
<div :class="ui.container({ class: [props.ui?.container, item.ui?.container] })">
|
||||
<StepperTrigger :class="ui.trigger({ class: [props.ui?.trigger, item.ui?.trigger] })">
|
||||
<StepperIndicator :class="ui.indicator({ class: [props.ui?.indicator, item.ui?.indicator] })">
|
||||
<slot name="indicator" :item="item">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.icon({ class: props.ui?.icon })" />
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.icon({ class: [props.ui?.icon, item.ui?.icon] })" />
|
||||
<template v-else>
|
||||
{{ count + 1 }}
|
||||
</template>
|
||||
@@ -152,17 +154,17 @@ defineExpose({
|
||||
|
||||
<StepperSeparator
|
||||
v-if="count < items.length - 1"
|
||||
:class="ui.separator({ class: props.ui?.separator })"
|
||||
:class="ui.separator({ class: [props.ui?.separator, item.ui?.separator] })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
|
||||
<StepperTitle :class="ui.title({ class: props.ui?.title })">
|
||||
<div :class="ui.wrapper({ class: [props.ui?.wrapper, item.ui?.wrapper] })">
|
||||
<StepperTitle as="div" :class="ui.title({ class: [props.ui?.title, item.ui?.title] })">
|
||||
<slot name="title" :item="item">
|
||||
{{ item.title }}
|
||||
</slot>
|
||||
</StepperTitle>
|
||||
<StepperDescription :class="ui.description({ class: props.ui?.description })">
|
||||
<StepperDescription as="div" :class="ui.description({ class: [props.ui?.description, item.ui?.description] })">
|
||||
<slot name="description" :item="item">
|
||||
{{ item.description }}
|
||||
</slot>
|
||||
|
||||
@@ -96,7 +96,7 @@ function onUpdate(value: any) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<div :class="ui.container({ class: props.ui?.container })">
|
||||
<SwitchRoot
|
||||
:id="id"
|
||||
|
||||
@@ -338,7 +338,7 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<table ref="tableRef" :class="ui.base({ class: [props.ui?.base] })">
|
||||
<caption v-if="caption || !!slots.caption" :class="ui.caption({ class: [props.ui?.caption] })">
|
||||
<slot name="caption">
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface TabsItem {
|
||||
/** A unique value for the tab item. Defaults to the index. */
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
class?: any
|
||||
ui?: Pick<Tabs['slots'], 'trigger' | 'leadingIcon' | 'leadingAvatar' | 'label' | 'content'>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -109,19 +111,19 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tabs || {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<TabsRoot v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<TabsList :class="ui.list({ class: props.ui?.list })">
|
||||
<TabsIndicator :class="ui.indicator({ class: props.ui?.indicator })" />
|
||||
|
||||
<slot name="list-leading" />
|
||||
|
||||
<TabsTrigger v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :disabled="item.disabled" :class="ui.trigger({ class: props.ui?.trigger })">
|
||||
<TabsTrigger v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :disabled="item.disabled" :class="ui.trigger({ class: [props.ui?.trigger, item.ui?.trigger] })">
|
||||
<slot name="leading" :item="item" :index="index">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: [props.ui?.leadingIcon, item.ui?.leadingIcon] })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.leadingAvatar({ class: [props.ui?.leadingAvatar, item.ui?.leadingAvatar] })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: props.ui?.label })">
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: [props.ui?.label, item.ui?.label] })">
|
||||
<slot :item="item" :index="index">{{ get(item, props.labelKey as string) }}</slot>
|
||||
</span>
|
||||
|
||||
@@ -132,7 +134,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tabs || {})
|
||||
</TabsList>
|
||||
|
||||
<template v-if="!!content">
|
||||
<TabsContent v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :class="ui.content({ class: props.ui?.content })">
|
||||
<TabsContent v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :class="ui.content({ class: [props.ui?.content, item.ui?.content, item.class] })">
|
||||
<slot :name="((item.slot || 'content') as keyof TabsSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index">
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AppConfig } from '@nuxt/schema'
|
||||
import theme from '#build/ui/textarea'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import type { AvatarProps } from '../types'
|
||||
import type { ComponentConfig } from '../types/utils'
|
||||
import type { AcceptableValue, ComponentConfig } from '../types/utils'
|
||||
|
||||
type Textarea = ComponentConfig<typeof theme, AppConfig, 'textarea'>
|
||||
|
||||
@@ -43,8 +43,8 @@ export interface TextareaProps extends UseComponentIconsProps {
|
||||
ui?: Textarea['slots']
|
||||
}
|
||||
|
||||
export interface TextareaEmits {
|
||||
(e: 'update:modelValue', payload: string | number): void
|
||||
export interface TextareaEmits<T extends AcceptableValue = AcceptableValue> {
|
||||
(e: 'update:modelValue', payload: T): void
|
||||
(e: 'blur', event: FocusEvent): void
|
||||
(e: 'change', event: Event): void
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export interface TextareaSlots {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="T extends AcceptableValue">
|
||||
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
@@ -73,12 +73,13 @@ const props = withDefaults(defineProps<TextareaProps>(), {
|
||||
autofocusDelay: 0,
|
||||
autoresizeDelay: 0
|
||||
})
|
||||
const emits = defineEmits<TextareaEmits<T>>()
|
||||
const slots = defineSlots<TextareaSlots>()
|
||||
const emits = defineEmits<TextareaEmits>()
|
||||
|
||||
const [modelValue, modelModifiers] = defineModel<string | number | null>()
|
||||
const [modelValue, modelModifiers] = defineModel<T>()
|
||||
|
||||
const appConfig = useAppConfig() as Textarea['AppConfig']
|
||||
|
||||
const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps>(props, { deferInputValidation: true })
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
|
||||
|
||||
@@ -109,7 +110,7 @@ function updateInput(value: string | null) {
|
||||
value ||= null
|
||||
}
|
||||
|
||||
modelValue.value = value
|
||||
modelValue.value = value as T
|
||||
emitFormInput()
|
||||
}
|
||||
|
||||
@@ -190,7 +191,7 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<textarea
|
||||
:id="id"
|
||||
ref="textareaRef"
|
||||
|
||||
@@ -29,6 +29,11 @@ export interface ToastProps extends Pick<ToastRootProps, 'defaultOpen' | 'open'
|
||||
* @defaultValue 'vertical'
|
||||
*/
|
||||
orientation?: Toast['variants']['orientation']
|
||||
/**
|
||||
* Whether to show the progress bar.
|
||||
* @defaultValue true
|
||||
*/
|
||||
progress?: boolean
|
||||
/**
|
||||
* Display a list of actions:
|
||||
* - under the title and description when orientation is `vertical`
|
||||
@@ -76,7 +81,8 @@ import UButton from './Button.vue'
|
||||
|
||||
const props = withDefaults(defineProps<ToastProps>(), {
|
||||
close: true,
|
||||
orientation: 'vertical'
|
||||
orientation: 'vertical',
|
||||
progress: true
|
||||
})
|
||||
const emits = defineEmits<ToastEmits>()
|
||||
const slots = defineSlots<ToastSlots>()
|
||||
@@ -116,7 +122,7 @@ defineExpose({
|
||||
v-slot="{ remaining, duration }"
|
||||
v-bind="rootProps"
|
||||
:data-orientation="orientation"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
:style="{ '--height': height }"
|
||||
>
|
||||
<slot name="leading">
|
||||
@@ -179,6 +185,6 @@ defineExpose({
|
||||
</ToastClose>
|
||||
</div>
|
||||
|
||||
<div v-if="remaining > 0 && duration" :class="ui.progress({ class: props.ui?.progress })" :style="{ width: `${remaining / duration * 100}%` }" />
|
||||
<div v-if="progress && remaining > 0 && duration" :class="ui.progress({ class: props.ui?.progress })" :style="{ width: `${remaining / duration * 100}%` }" />
|
||||
</ToastRoot>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,11 @@ export interface ToasterProps extends Omit<ToastProviderProps, 'swipeDirection'>
|
||||
* @defaultValue true
|
||||
*/
|
||||
expand?: boolean
|
||||
/**
|
||||
* Whether to show the progress bar on all toasts.
|
||||
* @defaultValue true
|
||||
*/
|
||||
progress?: boolean
|
||||
/**
|
||||
* Render the toaster in a portal.
|
||||
* @defaultValue true
|
||||
@@ -49,7 +54,8 @@ import UToast from './Toast.vue'
|
||||
const props = withDefaults(defineProps<ToasterProps>(), {
|
||||
expand: true,
|
||||
portal: true,
|
||||
duration: 5000
|
||||
duration: 5000,
|
||||
progress: true
|
||||
})
|
||||
defineSlots<ToasterSlots>()
|
||||
|
||||
@@ -109,6 +115,7 @@ function getOffset(index: number) {
|
||||
v-for="(toast, index) of toasts"
|
||||
:key="toast.id"
|
||||
ref="refs"
|
||||
:progress="progress"
|
||||
v-bind="omit(toast, ['id', 'close'])"
|
||||
:close="(toast.close as boolean)"
|
||||
:data-expanded="expanded"
|
||||
@@ -121,9 +128,7 @@ function getOffset(index: number) {
|
||||
'--translate': expanded ? 'calc(var(--offset) * var(--translate-factor))' : 'calc(var(--before) * var(--gap))',
|
||||
'--transform': 'translateY(var(--translate)) scale(var(--scale))'
|
||||
}"
|
||||
:class="[ui.base(), {
|
||||
'cursor-pointer': !!toast.onClick
|
||||
}]"
|
||||
:class="ui.base({ class: [props.ui?.base, toast.onClick ? 'cursor-pointer' : undefined] })"
|
||||
@update:open="onUpdateOpen($event, toast.id)"
|
||||
@click="toast.onClick && toast.onClick(toast)"
|
||||
/>
|
||||
@@ -131,7 +136,7 @@ function getOffset(index: number) {
|
||||
<ToastPortal v-bind="portalProps">
|
||||
<ToastViewport
|
||||
:data-expanded="expanded"
|
||||
:class="ui.viewport({ class: [props.class, props.ui?.viewport] })"
|
||||
:class="ui.viewport({ class: [props.ui?.viewport, props.class] })"
|
||||
:style="{
|
||||
'--scale-factor': '0.05',
|
||||
'--translate-factor': position?.startsWith('top') ? '1px' : '-1px',
|
||||
|
||||
@@ -24,6 +24,8 @@ export type TreeItem = {
|
||||
children?: TreeItem[]
|
||||
onToggle?(e: Event): void
|
||||
onSelect?(e?: Event): void
|
||||
class?: any
|
||||
ui?: Pick<Tree['slots'], 'item' | 'itemWithChildren' | 'link' | 'linkLeadingIcon' | 'linkLabel' | 'linkTrailing' | 'linkTrailingIcon' | 'listWithChildren'>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -149,7 +151,7 @@ const defaultExpanded = computed(() =>
|
||||
<li
|
||||
v-for="(item, index) in items"
|
||||
:key="`${level}-${index}`"
|
||||
:class="level > 0 ? ui.itemWithChildren({ class: props.ui?.itemWithChildren }) : ui.item({ class: props.ui?.item })"
|
||||
:class="level > 0 ? ui.itemWithChildren({ class: [props.ui?.itemWithChildren, item.ui?.itemWithChildren] }) : ui.item({ class: [props.ui?.item, item.ui?.item] })"
|
||||
>
|
||||
<TreeItem
|
||||
v-slot="{ isExpanded, isSelected }"
|
||||
@@ -159,37 +161,37 @@ const defaultExpanded = computed(() =>
|
||||
@toggle="item.onToggle"
|
||||
@select="item.onSelect"
|
||||
>
|
||||
<button :disabled="item.disabled || disabled" :class="ui.link({ class: props.ui?.link, selected: isSelected, disabled: item.disabled || disabled })">
|
||||
<button :disabled="item.disabled || disabled" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], selected: isSelected, disabled: item.disabled || disabled })">
|
||||
<slot :name="((item.slot || 'item') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||
<UIcon
|
||||
v-if="item.icon"
|
||||
:name="item.icon"
|
||||
:class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon })"
|
||||
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
|
||||
/>
|
||||
<UIcon
|
||||
v-else-if="item.children?.length"
|
||||
:name="isExpanded ? (expandedIcon ?? appConfig.ui.icons.folderOpen) : (collapsedIcon ?? appConfig.ui.icons.folder)"
|
||||
:class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon })"
|
||||
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<span v-if="getItemLabel(item) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>]" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
||||
<span v-if="getItemLabel(item) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>]" :class="ui.linkLabel({ class: [props.ui?.linkLabel, item.ui?.linkLabel] })">
|
||||
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||
{{ getItemLabel(item) }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span v-if="item.trailingIcon || item.children?.length || !!slots[(item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>]" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
||||
<span v-if="item.trailingIcon || item.children?.length || !!slots[(item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>]" :class="ui.linkTrailing({ class: [props.ui?.linkTrailing, item.ui?.linkTrailing] })">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||
<UIcon v-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })" />
|
||||
<UIcon v-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })" />
|
||||
<UIcon v-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
|
||||
<UIcon v-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
|
||||
</slot>
|
||||
</span>
|
||||
</slot>
|
||||
</button>
|
||||
|
||||
<ul v-if="item.children?.length && isExpanded" :class="ui.listWithChildren({ class: props.ui?.listWithChildren })">
|
||||
<ul v-if="item.children?.length && isExpanded" :class="ui.listWithChildren({ class: [props.ui?.listWithChildren, item.ui?.listWithChildren] })">
|
||||
<ReuseTreeTemplate :items="item.children" :level="level + 1" />
|
||||
</ul>
|
||||
</TreeItem>
|
||||
@@ -198,7 +200,7 @@ const defaultExpanded = computed(() =>
|
||||
|
||||
<TreeRoot
|
||||
v-bind="(rootProps as unknown as TreeRootProps<NestedItem<T>>)"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
:get-key="getItemValue"
|
||||
:default-expanded="defaultExpanded"
|
||||
:selection-behavior="selectionBehavior"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { inject, computed, type InjectionKey, type Ref, type ComputedRef } from 'vue'
|
||||
import { inject, computed, type InjectionKey, type Ref, type ComputedRef, provide } from 'vue'
|
||||
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
|
||||
import type { FormFieldProps } from '../types'
|
||||
import type { FormEvent, FormInputEvents, FormFieldInjectedOptions, FormInjectedOptions } from '../types/form'
|
||||
@@ -15,7 +15,7 @@ type Props<T> = {
|
||||
|
||||
export const formOptionsInjectionKey: InjectionKey<ComputedRef<FormInjectedOptions>> = Symbol('nuxt-ui.form-options')
|
||||
export const formBusInjectionKey: InjectionKey<UseEventBusReturn<FormEvent<any>, string>> = Symbol('nuxt-ui.form-events')
|
||||
export const formFieldInjectionKey: InjectionKey<ComputedRef<FormFieldInjectedOptions<FormFieldProps>>> = Symbol('nuxt-ui.form-field')
|
||||
export const formFieldInjectionKey: InjectionKey<ComputedRef<FormFieldInjectedOptions<FormFieldProps>> | undefined> = Symbol('nuxt-ui.form-field')
|
||||
export const inputIdInjectionKey: InjectionKey<Ref<string | undefined>> = Symbol('nuxt-ui.input-id')
|
||||
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, { id?: string, pattern?: RegExp }>>> = Symbol('nuxt-ui.form-inputs')
|
||||
export const formLoadingInjectionKey: InjectionKey<Readonly<Ref<boolean>>> = Symbol('nuxt-ui.form-loading')
|
||||
@@ -27,6 +27,9 @@ export function useFormField<T>(props?: Props<T>, opts?: { bind?: boolean, defer
|
||||
const formInputs = inject(formInputsInjectionKey, undefined)
|
||||
const inputId = inject(inputIdInjectionKey, undefined)
|
||||
|
||||
// Blocks the FormField injection to avoid duplicating events when nesting input components.
|
||||
provide(formFieldInjectionKey, undefined)
|
||||
|
||||
if (formField && inputId) {
|
||||
if (opts?.bind === false) {
|
||||
// Removes for="..." attribute on label for RadioGroup and alike.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user