mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-21 15:31:46 +01:00
Compare commits
88 Commits
content-3.
...
deps/nuxt4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afcf86ac63 | ||
|
|
b13a4370da | ||
|
|
5b0ffeac5e | ||
|
|
55e06e97e7 | ||
|
|
a813ea700e | ||
|
|
a4d0ca7396 | ||
|
|
5ad7dabbdc | ||
|
|
d8160ba6ef | ||
|
|
fc24e03cc4 | ||
|
|
1902492cf2 | ||
|
|
0c525638d7 | ||
|
|
35f90b9920 | ||
|
|
836f74849b | ||
|
|
78f92a24f8 | ||
|
|
52908c19f1 | ||
|
|
513cca25f6 | ||
|
|
c1427a3264 | ||
|
|
6519a74de4 | ||
|
|
da05c37ffe | ||
|
|
ec569e427b | ||
|
|
1d052ec565 | ||
|
|
1ba8a55bcb | ||
|
|
63730d684b | ||
|
|
9ab184cc24 | ||
|
|
ad0e4ddbf4 | ||
|
|
6a93556aed | ||
|
|
9debce737c | ||
|
|
772631cde9 | ||
|
|
d7aefa53b2 | ||
|
|
8922c7388e | ||
|
|
c355cacd43 | ||
|
|
a0e71d9e29 | ||
|
|
127e06ae83 | ||
|
|
09c1ed8bf4 | ||
|
|
a05102fab3 | ||
|
|
09caf44d0d | ||
|
|
15482aae76 | ||
|
|
f903ec396f | ||
|
|
b00e07f13d | ||
|
|
5c573b37b6 | ||
|
|
f62c5ec20c | ||
|
|
b96a1ccbab | ||
|
|
4ce654076c | ||
|
|
fb9e7bb856 | ||
|
|
69a7b957d5 | ||
|
|
3b67d54833 | ||
|
|
df8f20232f | ||
|
|
347694b4b5 | ||
|
|
021880328b | ||
|
|
9c1f423555 | ||
|
|
6cb737e038 | ||
|
|
231b82fe4c | ||
|
|
57a5037b13 | ||
|
|
752e2b69bd | ||
|
|
6237663a01 | ||
|
|
44cfa00e4d | ||
|
|
8cbbab9a6b | ||
|
|
2d51e20939 | ||
|
|
268e29b041 | ||
|
|
b0364b96b7 | ||
|
|
ba3c6e8788 | ||
|
|
01da3cbf31 | ||
|
|
595fc64515 | ||
|
|
81569713e9 | ||
|
|
1a8feb751e | ||
|
|
1d281e915a | ||
|
|
c3adc381c9 | ||
|
|
edca3bcb74 | ||
|
|
8f32ee3d24 | ||
|
|
9172bb7dc2 | ||
|
|
32dae2e002 | ||
|
|
be41aed1f3 | ||
|
|
bf678412ca | ||
|
|
a999600e9f | ||
|
|
04f12adc5b | ||
|
|
abfd0ede03 | ||
|
|
2fa8db64dd | ||
|
|
52f1963833 | ||
|
|
9a83c9c7f4 | ||
|
|
f2510cb342 | ||
|
|
4dd56c8111 | ||
|
|
6e3ec6a077 | ||
|
|
59c26ec123 | ||
|
|
67ef866a40 | ||
|
|
5170cfd7eb | ||
|
|
9bcf1ad92f | ||
|
|
7a2bd4e617 | ||
|
|
8781a07909 |
46
CHANGELOG.md
46
CHANGELOG.md
@@ -1,5 +1,51 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [3.2.0](https://github.com/nuxt/ui/compare/v3.1.3...v3.2.0) (2025-06-25)
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* **useOverlay:** correct spelling of `unmount` function (#4051)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Avatar:** add `chip` prop ([#4224](https://github.com/nuxt/ui/issues/4224)) ([03ac395](https://github.com/nuxt/ui/commit/03ac395164c02c964361c68743268b1bc90aae59))
|
||||||
|
* **Carousel:** allow customization of active dot color ([#4229](https://github.com/nuxt/ui/issues/4229)) ([2ee1c5a](https://github.com/nuxt/ui/commit/2ee1c5ac2e20ab9ce2f4037a8e8c64e561b0428b))
|
||||||
|
* **CommandPalette:** handle `children` in items ([#4226](https://github.com/nuxt/ui/issues/4226)) ([59c26ec](https://github.com/nuxt/ui/commit/59c26ec1230375a24fbaf8a630a696ae854700c7))
|
||||||
|
* **extendLocale:** new composable ([0f558fc](https://github.com/nuxt/ui/commit/0f558fc0d014d51549222accfc50286d1770d1aa)), closes [#3729](https://github.com/nuxt/ui/issues/3729)
|
||||||
|
* **Form:** expose loading state to default slot ([#4247](https://github.com/nuxt/ui/issues/4247)) ([ea0c459](https://github.com/nuxt/ui/commit/ea0c459306be585bacaaf5b433114d072550c824))
|
||||||
|
* **InputTags:** new component ([#4261](https://github.com/nuxt/ui/issues/4261)) ([54bb228](https://github.com/nuxt/ui/commit/54bb2282c58d3bf5a7dde4cdee687c68efd934a0))
|
||||||
|
* **locale:** add Luxembourgish language ([#4264](https://github.com/nuxt/ui/issues/4264)) ([43cbb94](https://github.com/nuxt/ui/commit/43cbb94ee25106b414fc8fe979fa65ebaa9ccc76))
|
||||||
|
* **Modal/Slideover:** add `actions` slot ([#4358](https://github.com/nuxt/ui/issues/4358)) ([8156971](https://github.com/nuxt/ui/commit/81569713e9da9d5531ecdf4614660b84c686fa81))
|
||||||
|
* **Modal/Slideover:** add `close` method in slots ([#4219](https://github.com/nuxt/ui/issues/4219)) ([5835eb5](https://github.com/nuxt/ui/commit/5835eb5f0f835b5f03646dec78f85b2f556a109b))
|
||||||
|
* **Select/SelectMenu/Tabs:** expose trigger refs ([7a2bd4e](https://github.com/nuxt/ui/commit/7a2bd4e6179373902ba6f285903ea896fd1d378f)), closes [#4292](https://github.com/nuxt/ui/issues/4292)
|
||||||
|
* **Select/SelectMenu:** handle dynamic `autofocus` ([1a4de49](https://github.com/nuxt/ui/commit/1a4de49c1665c9ef65279315be0393d6272447b9)), closes [#4324](https://github.com/nuxt/ui/issues/4324)
|
||||||
|
* **Table:** add `body-top` / `body-bottom` slots ([#4354](https://github.com/nuxt/ui/issues/4354)) ([595fc64](https://github.com/nuxt/ui/commit/595fc64515613fe82c3a56fc5518f2e3fcce6e19))
|
||||||
|
* **Timeline:** add `reverse` prop ([#4316](https://github.com/nuxt/ui/issues/4316)) ([5170cfd](https://github.com/nuxt/ui/commit/5170cfd7eb44a25c64673cf12979f9ca1049695f))
|
||||||
|
* **Timeline:** new component ([#4215](https://github.com/nuxt/ui/issues/4215)) ([8017767](https://github.com/nuxt/ui/commit/80177679f2aa0d7f0e39e639a02d527a06e6172c))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Card/Drawer/Modal:** prevent scrollbars overflow ([#4368](https://github.com/nuxt/ui/issues/4368)) ([c3adc38](https://github.com/nuxt/ui/commit/c3adc381c90dad7152e27fc303ee678efc7c4c94))
|
||||||
|
* **components:** remove default `md` size on buttons ([#4357](https://github.com/nuxt/ui/issues/4357)) ([be41aed](https://github.com/nuxt/ui/commit/be41aed1f3d3476801e1840dbb8766926bc93c05))
|
||||||
|
* **defineShortcuts:** allow `meta_-` shortcut ([#4321](https://github.com/nuxt/ui/issues/4321)) ([4e7c1c9](https://github.com/nuxt/ui/commit/4e7c1c9c305b45dd76d4c238e70a6aeedae78c8b))
|
||||||
|
* **Form:** conditionally type form data via `transform` prop ([#4188](https://github.com/nuxt/ui/issues/4188)) ([37abcc6](https://github.com/nuxt/ui/commit/37abcc6a5b0a678be626673af5067956657a50d6))
|
||||||
|
* **Form:** expose reactive fields ([#4386](https://github.com/nuxt/ui/issues/4386)) ([1a8feb7](https://github.com/nuxt/ui/commit/1a8feb751e6827c414ef82fe9fb259ba7dcc7e08))
|
||||||
|
* **InputMenu/SelectMenu:** dynamic `empty` size ([ba3c6e8](https://github.com/nuxt/ui/commit/ba3c6e8788ed75d86d4406749797da52d7816b84)), closes [#4377](https://github.com/nuxt/ui/issues/4377)
|
||||||
|
* **InputTags:** extend emits interface ([8781a07](https://github.com/nuxt/ui/commit/8781a079096def0d3bae5b8d896db0df6ce37e23))
|
||||||
|
* **Modal/Slideover:** don't emit `close:prevent` on `closeAutoFocus` ([150b334](https://github.com/nuxt/ui/commit/150b334b1d242c6dc132193e23359c03e6f35666))
|
||||||
|
* **NavigationMenu:** nested accordion context at every level ([#4363](https://github.com/nuxt/ui/issues/4363)) ([2fa8db6](https://github.com/nuxt/ui/commit/2fa8db64ddf4c92a19e73774143518d87d001b72))
|
||||||
|
* **NavigationMenu:** set content `max-height` in `horizontal` orientation ([62bc7b2](https://github.com/nuxt/ui/commit/62bc7b25a2d205d8dffb47a109196f91ff3e823a)), closes [#4208](https://github.com/nuxt/ui/issues/4208)
|
||||||
|
* **Pagination:** match default button `size` ([#4350](https://github.com/nuxt/ui/issues/4350)) ([4dd56c8](https://github.com/nuxt/ui/commit/4dd56c8111e5a224105b82d541b7742b46abb34a))
|
||||||
|
* **Select/SelectMenu:** display falsy values ([7df7ee3](https://github.com/nuxt/ui/commit/7df7ee336a925d7ee07f866551dad9350785c9fc))
|
||||||
|
* **Select/SelectMenu:** prevent empty string display when multiple ([483e473](https://github.com/nuxt/ui/commit/483e473e3f5681cc97c3766ea47283dc95f76345))
|
||||||
|
* **SelectMenu:** dynamic input size ([b0364b9](https://github.com/nuxt/ui/commit/b0364b96b73b9e543781a35962c03b5a983352c4))
|
||||||
|
* **Table:** use `tr` as separator ([#4083](https://github.com/nuxt/ui/issues/4083)) ([edca3bc](https://github.com/nuxt/ui/commit/edca3bcb743c7eb63e6abbaa801d3858342a8777))
|
||||||
|
* **Toast:** calc height on next tick ([3bf5acb](https://github.com/nuxt/ui/commit/3bf5acb683f0ad09735b2417d265d6fcfd901b11)), closes [#4265](https://github.com/nuxt/ui/issues/4265)
|
||||||
|
* **Toaster:** smoother visibility transition for stacked toasts ([#4367](https://github.com/nuxt/ui/issues/4367)) ([abfd0ed](https://github.com/nuxt/ui/commit/abfd0ede036fa2953f9abc841d77ac71bbd3bba9))
|
||||||
|
* **useOverlay:** correct spelling of `unmount` function ([#4051](https://github.com/nuxt/ui/issues/4051)) ([546df57](https://github.com/nuxt/ui/commit/546df572fca60325315bed17c9be3367052fb7a9))
|
||||||
|
* **useOverlay:** set props to original props when `defaultOpen` is set ([#4308](https://github.com/nuxt/ui/issues/4308)) ([66355ba](https://github.com/nuxt/ui/commit/66355ba301d569b9f44527bafc5f8f09bcda63c0))
|
||||||
|
* **useOverlay:** use original props when not provided to `open` ([#4269](https://github.com/nuxt/ui/issues/4269)) ([bf56e15](https://github.com/nuxt/ui/commit/bf56e15a2eed7d51199d5641649a822e91ca41ba))
|
||||||
|
|
||||||
## [3.1.3](https://github.com/nuxt/ui/compare/v3.1.2...v3.1.3) (2025-05-26)
|
## [3.1.3](https://github.com/nuxt/ui/compare/v3.1.2...v3.1.3) (2025-05-26)
|
||||||
|
|
||||||
### ⚠ BREAKING CHANGES
|
### ⚠ BREAKING CHANGES
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ const component = ({ name, primitive, pro, prose, content }) => {
|
|||||||
? `
|
? `
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
|
${pro ? `import type { ComponentConfig } from '@nuxt/ui'` : ''}
|
||||||
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
|
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
|
||||||
import type { ComponentConfig } from '../types/utils'
|
${!pro ? `import type { ComponentConfig } from '../types/utils'` : ''}
|
||||||
|
|
||||||
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}${pro ? `, '${key}'` : ''}>
|
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}${pro ? `, '${key}'` : ''}>
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ defineSlots<${upperName}Slots>()
|
|||||||
|
|
||||||
const appConfig = useAppConfig() as ${upperName}['AppConfig']
|
const appConfig = useAppConfig() as ${upperName}['AppConfig']
|
||||||
|
|
||||||
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName} || {}) })())
|
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.${pro ? 'uiPro' : 'ui'}?.${camelName} || {}) })())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -75,8 +76,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName}
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ${upperName}RootProps, ${upperName}RootEmits } from 'reka-ui'
|
import type { ${upperName}RootProps, ${upperName}RootEmits } from 'reka-ui'
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
|
${pro ? `import type { ComponentConfig } from '@nuxt/ui'` : ''}
|
||||||
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
|
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
|
||||||
import type { ComponentConfig } from '../types/utils'
|
${!pro ? `import type { ComponentConfig } from '../types/utils'` : ''}
|
||||||
|
|
||||||
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}${pro ? `, '${key}'` : ''}>
|
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}${pro ? `, '${key}'` : ''}>
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ const appConfig = useAppConfig() as ${upperName}['AppConfig']
|
|||||||
|
|
||||||
const rootProps = useForwardPropsEmits(reactivePick(props), emits)
|
const rootProps = useForwardPropsEmits(reactivePick(props), emits)
|
||||||
|
|
||||||
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName} || {}) })())
|
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.${pro ? 'uiPro' : 'ui'}?.${camelName} || {}) })())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -145,7 +147,8 @@ const test = ({ name, prose, content }) => {
|
|||||||
? undefined
|
? undefined
|
||||||
: `
|
: `
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import ${upperName}, { type ${upperName}Props, type ${upperName}Slots } from '../../${content ? '../' : ''}src/runtime/components/${content ? 'content/' : ''}${upperName}.vue'
|
import ${upperName} from '../../${content ? '../' : ''}src/runtime/components/${content ? 'content/' : ''}${upperName}.vue'
|
||||||
|
import type { ${upperName}Props, ${upperName}Slots } from '../../${content ? '../' : ''}src/runtime/components/${content ? 'content/' : ''}${upperName}.vue'
|
||||||
import ComponentRender from '../${content ? '../' : ''}component-render'
|
import ComponentRender from '../${content ? '../' : ''}component-render'
|
||||||
|
|
||||||
describe('${upperName}', () => {
|
describe('${upperName}', () => {
|
||||||
@@ -186,6 +189,7 @@ links:${primitive
|
|||||||
- label: GitHub
|
- label: GitHub
|
||||||
icon: i-simple-icons-github
|
icon: i-simple-icons-github
|
||||||
to: https://github.com/nuxt/${pro ? 'ui-pro' : 'ui'}/tree/v3/src/runtime/components/${upperName}.vue
|
to: https://github.com/nuxt/${pro ? 'ui-pro' : 'ui'}/tree/v3/src/runtime/components/${upperName}.vue
|
||||||
|
navigation.badge: Soon
|
||||||
---
|
---
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ provide('navigation', mappedNavigation)
|
|||||||
<NuxtLoadingIndicator color="var(--ui-primary)" :height="2" />
|
<NuxtLoadingIndicator color="var(--ui-primary)" :height="2" />
|
||||||
|
|
||||||
<template v-if="!route.path.startsWith('/examples')">
|
<template v-if="!route.path.startsWith('/examples')">
|
||||||
<!-- <Banner /> -->
|
<Banner />
|
||||||
|
|
||||||
<Header :links="links" />
|
<Header :links="links" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<UBanner
|
<UBanner
|
||||||
id="ui3-launch"
|
id="nuxtlabs-join-vercel"
|
||||||
icon="i-lucide-rocket"
|
title="NuxtLabs is joining Vercel"
|
||||||
:actions="[
|
icon="i-simple-icons-vercel"
|
||||||
{
|
to="https://nuxtlabs.com/?utm_source=nuxt-ui&utm_medium=banner&utm_campaign=nuxtlabs-vercel"
|
||||||
label: 'Discover Nuxt UI Pro',
|
target="_blank"
|
||||||
to: '/pro/pricing',
|
|
||||||
trailingIcon: 'i-lucide-arrow-right'
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
close
|
close
|
||||||
>
|
:actions="[{
|
||||||
<template #title>
|
label: 'Read the announcement',
|
||||||
<span class="font-semibold">Nuxt UI v3</span> is officially released.
|
color: 'neutral',
|
||||||
</template>
|
variant: 'outline',
|
||||||
</UBanner>
|
trailingIcon: 'i-lucide-arrow-right',
|
||||||
|
to: 'https://nuxtlabs.com/?utm_source=nuxt-ui&utm_medium=banner&utm_campaign=nuxtlabs-vercel',
|
||||||
|
target: '_blank',
|
||||||
|
class: 'ring-0'
|
||||||
|
}]"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
77
docs/app/components/PageHeaderLinks.vue
Normal file
77
docs/app/components/PageHeaderLinks.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute()
|
||||||
|
const toast = useToast()
|
||||||
|
const { copy, copied } = useClipboard()
|
||||||
|
const site = useSiteConfig()
|
||||||
|
|
||||||
|
const mdPath = computed(() => `${site.url}/raw${route.path}.md`)
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'Copy Markdown link',
|
||||||
|
icon: 'i-lucide-link',
|
||||||
|
onSelect() {
|
||||||
|
copy(mdPath.value)
|
||||||
|
toast.add({
|
||||||
|
title: 'Copied to clipboard',
|
||||||
|
icon: 'i-lucide-check-circle'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'View as Markdown',
|
||||||
|
icon: 'i-simple-icons:markdown',
|
||||||
|
target: '_blank',
|
||||||
|
to: `/raw${route.path}.md`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Open in ChatGPT',
|
||||||
|
icon: 'i-simple-icons:openai',
|
||||||
|
target: '_blank',
|
||||||
|
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Open in Claude',
|
||||||
|
icon: 'i-simple-icons:anthropic',
|
||||||
|
target: '_blank',
|
||||||
|
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
async function copyPage() {
|
||||||
|
copy(await $fetch<string>(`/raw${route.path}.md`))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UButtonGroup>
|
||||||
|
<UButton
|
||||||
|
label="Copy page"
|
||||||
|
:icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
:ui="{
|
||||||
|
leadingIcon: [copied ? 'text-primary' : 'text-neutral', 'size-3.5']
|
||||||
|
}"
|
||||||
|
@click="copyPage"
|
||||||
|
/>
|
||||||
|
<UDropdownMenu
|
||||||
|
:items="items"
|
||||||
|
:content="{
|
||||||
|
align: 'end',
|
||||||
|
side: 'bottom',
|
||||||
|
sideOffset: 8
|
||||||
|
}"
|
||||||
|
:ui="{
|
||||||
|
content: 'w-48'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-chevron-down"
|
||||||
|
size="sm"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
/>
|
||||||
|
</UDropdownMenu>
|
||||||
|
</UButtonGroup>
|
||||||
|
</template>
|
||||||
@@ -34,7 +34,7 @@ const meta = await fetchComponentMeta(name as any)
|
|||||||
</ProseCode>
|
</ProseCode>
|
||||||
</ProseTd>
|
</ProseTd>
|
||||||
<ProseTd>
|
<ProseTd>
|
||||||
<HighlightInlineType v-if="slot.type" :type="slot.type" />
|
<HighlightInlineType v-if="slot.type" :type="slot.type.replace(/ui:\s*\{[^}]*\}/g, 'ui: {}')" />
|
||||||
|
|
||||||
<MDC v-if="slot.description" :value="slot.description" class="text-toned mt-1" :cache-key="`${kebabCase(route.path)}-${slot.name}-description`" />
|
<MDC v-if="slot.description" :value="slot.description" class="text-toned mt-1" :cache-key="`${kebabCase(route.path)}-${slot.name}-description`" />
|
||||||
</ProseTd>
|
</ProseTd>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ const items = [
|
|||||||
v-slot="{ item }"
|
v-slot="{ item }"
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
:items="items"
|
:items="items"
|
||||||
class="w-full max-w-xs mx-auto"
|
|
||||||
:ui="{ container: 'h-[336px]' }"
|
:ui="{ container: 'h-[336px]' }"
|
||||||
|
class="w-full max-w-xs mx-auto"
|
||||||
>
|
>
|
||||||
<img :src="item" width="320" height="320" class="rounded-lg">
|
<img :src="item" width="320" height="320" class="rounded-lg">
|
||||||
</UCarousel>
|
</UCarousel>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const groups = [
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #billing-label="{ item }">
|
<template #billing-label="{ item }">
|
||||||
{{ item.label }}
|
<span class="font-medium text-primary">{{ item.label }}</span>
|
||||||
|
|
||||||
<UBadge variant="subtle" size="sm">
|
<UBadge variant="subtle" size="sm">
|
||||||
50% off
|
50% off
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const groups = [
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Add new file',
|
||||||
|
suffix: 'Create a new file in the current directory',
|
||||||
|
icon: 'i-lucide-file-plus',
|
||||||
|
kbds: ['meta', 'N']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add new folder',
|
||||||
|
suffix: 'Create a new folder in the current directory',
|
||||||
|
icon: 'i-lucide-folder-plus',
|
||||||
|
kbds: ['meta', 'F']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Search files',
|
||||||
|
suffix: 'Search across all files in the project',
|
||||||
|
icon: 'i-lucide-search',
|
||||||
|
kbds: ['meta', 'P']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
suffix: 'Open application settings',
|
||||||
|
icon: 'i-lucide-settings',
|
||||||
|
kbds: ['meta', ',']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'recent',
|
||||||
|
label: 'Recent',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'project.vue',
|
||||||
|
suffix: 'components/',
|
||||||
|
icon: 'i-vscode-icons-file-type-vue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'readme.md',
|
||||||
|
suffix: 'docs/',
|
||||||
|
icon: 'i-vscode-icons-file-type-markdown'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'package.json',
|
||||||
|
suffix: 'root/',
|
||||||
|
icon: 'i-vscode-icons-file-type-node'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UCommandPalette :groups="groups" class="flex-1 h-80">
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<UIcon name="i-simple-icons-nuxtdotjs" class="size-5 text-dimmed ml-1" />
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<UButton color="neutral" variant="ghost" label="Open Command" class="text-dimmed" size="xs">
|
||||||
|
<template #trailing>
|
||||||
|
<UKbd value="enter" />
|
||||||
|
</template>
|
||||||
|
</UButton>
|
||||||
|
<USeparator orientation="vertical" class="h-4" />
|
||||||
|
<UButton color="neutral" variant="ghost" label="Actions" class="text-dimmed" size="xs">
|
||||||
|
<template #trailing>
|
||||||
|
<UKbd value="meta" />
|
||||||
|
<UKbd value="k" />
|
||||||
|
</template>
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCommandPalette>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const groups = [{
|
||||||
|
id: 'actions',
|
||||||
|
label: 'Actions',
|
||||||
|
items: [{
|
||||||
|
label: 'Create new',
|
||||||
|
icon: 'i-lucide-plus',
|
||||||
|
children: [{
|
||||||
|
label: 'New file',
|
||||||
|
icon: 'i-lucide-file-plus',
|
||||||
|
suffix: 'Create a new file in the current directory',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'New file created!' })
|
||||||
|
},
|
||||||
|
kbds: ['meta', 'N']
|
||||||
|
}, {
|
||||||
|
label: 'New folder',
|
||||||
|
icon: 'i-lucide-folder-plus',
|
||||||
|
suffix: 'Create a new folder in the current directory',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'New folder created!' })
|
||||||
|
},
|
||||||
|
kbds: ['meta', 'F']
|
||||||
|
}, {
|
||||||
|
label: 'New project',
|
||||||
|
icon: 'i-lucide-folder-git',
|
||||||
|
suffix: 'Create a new project from a template',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'New project created!' })
|
||||||
|
},
|
||||||
|
kbds: ['meta', 'P']
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
label: 'Share',
|
||||||
|
icon: 'i-lucide-share',
|
||||||
|
children: [{
|
||||||
|
label: 'Copy link',
|
||||||
|
icon: 'i-lucide-link',
|
||||||
|
suffix: 'Copy a link to the current item',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'Link copied to clipboard!' })
|
||||||
|
},
|
||||||
|
kbds: ['meta', 'L']
|
||||||
|
}, {
|
||||||
|
label: 'Share via email',
|
||||||
|
icon: 'i-lucide-mail',
|
||||||
|
suffix: 'Share the current item via email',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'Share via email dialog opened!' })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Share on social',
|
||||||
|
icon: 'i-lucide-share-2',
|
||||||
|
suffix: 'Share the current item on social media',
|
||||||
|
children: [{
|
||||||
|
label: 'Twitter',
|
||||||
|
icon: 'i-simple-icons-twitter',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'Shared on Twitter!' })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'LinkedIn',
|
||||||
|
icon: 'i-simple-icons-linkedin',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'Shared on LinkedIn!' })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Facebook',
|
||||||
|
icon: 'i-simple-icons-facebook',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'Shared on Facebook!' })
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
label: 'Settings',
|
||||||
|
icon: 'i-lucide-settings',
|
||||||
|
children: [{
|
||||||
|
label: 'General',
|
||||||
|
icon: 'i-lucide-sliders',
|
||||||
|
suffix: 'Configure general settings',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'General settings opened!' })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Appearance',
|
||||||
|
icon: 'i-lucide-palette',
|
||||||
|
suffix: 'Customize the appearance',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'Appearance settings opened!' })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Security',
|
||||||
|
icon: 'i-lucide-shield',
|
||||||
|
suffix: 'Manage security settings',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
toast.add({ title: 'Security settings opened!' })
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UCommandPalette :groups="groups" class="flex-1" />
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { object, string, nonempty, refine, type Infer } from 'superstruct'
|
import { object, string, nonempty, refine } from 'superstruct'
|
||||||
|
import type { Infer } from 'superstruct'
|
||||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||||
|
|
||||||
const schema = object({
|
const schema = object({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { object, string, type InferType } from 'yup'
|
import { object, string } from 'yup'
|
||||||
|
import type { InferType } from 'yup'
|
||||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||||
|
|
||||||
const schema = object({
|
const schema = object({
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { data: users } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
|
key: 'typicode-users-email',
|
||||||
|
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||||
|
return data?.map(user => ({
|
||||||
|
label: user.name,
|
||||||
|
value: String(user.id),
|
||||||
|
email: user.email,
|
||||||
|
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
lazy: true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UInputMenu
|
||||||
|
:items="users"
|
||||||
|
icon="i-lucide-user"
|
||||||
|
placeholder="Select user"
|
||||||
|
:ui="{ content: 'min-w-fit' }"
|
||||||
|
>
|
||||||
|
<template #item-label="{ item }">
|
||||||
|
{{ item.label }}
|
||||||
|
|
||||||
|
<span class="text-muted">
|
||||||
|
{{ item.email }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</UInputMenu>
|
||||||
|
</template>
|
||||||
@@ -35,6 +35,7 @@ const items = ref([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
] satisfies InputMenuItem[])
|
] satisfies InputMenuItem[])
|
||||||
|
|
||||||
const value = ref(items.value[0])
|
const value = ref(items.value[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
|
||||||
const value = ref('npx nuxt module add ui')
|
const value = ref('npx nuxt module add ui')
|
||||||
const copied = ref(false)
|
|
||||||
|
|
||||||
function copy() {
|
const { copy, copied } = useClipboard()
|
||||||
navigator.clipboard.writeText(value.value)
|
|
||||||
copied.value = true
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
copied.value = false
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -25,7 +19,7 @@ function copy() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
:icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
|
:icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
|
||||||
aria-label="Copy to clipboard"
|
aria-label="Copy to clipboard"
|
||||||
@click="copy"
|
@click="copy(value)"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ const text = computed(() => {
|
|||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
:color="color"
|
:color="color"
|
||||||
:type="show ? 'text' : 'password'"
|
:type="show ? 'text' : 'password'"
|
||||||
:ui="{ trailing: 'pe-1' }"
|
|
||||||
:aria-invalid="score < 4"
|
:aria-invalid="score < 4"
|
||||||
aria-describedby="password-strength"
|
aria-describedby="password-strength"
|
||||||
|
:ui="{ trailing: 'pe-1' }"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<template #trailing>
|
<template #trailing>
|
||||||
|
|||||||
@@ -24,3 +24,10 @@ const password = ref('')
|
|||||||
</template>
|
</template>
|
||||||
</UInput>
|
</UInput>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Hide the password reveal button in Edge */
|
||||||
|
::-ms-reveal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -62,13 +62,13 @@ const items = [
|
|||||||
<template>
|
<template>
|
||||||
<UNavigationMenu
|
<UNavigationMenu
|
||||||
:items="items"
|
:items="items"
|
||||||
class="w-full justify-center"
|
|
||||||
:ui="{
|
:ui="{
|
||||||
viewport: 'sm:w-(--reka-navigation-menu-viewport-width)',
|
viewport: 'sm:w-(--reka-navigation-menu-viewport-width)',
|
||||||
content: 'sm:w-auto',
|
content: 'sm:w-auto',
|
||||||
childList: 'sm:w-96',
|
childList: 'sm:w-96',
|
||||||
childLinkDescription: 'text-balance line-clamp-2'
|
childLinkDescription: 'text-balance line-clamp-2'
|
||||||
}"
|
}"
|
||||||
|
class="w-full justify-center"
|
||||||
>
|
>
|
||||||
<template #docs-content="{ item }">
|
<template #docs-content="{ item }">
|
||||||
<ul class="grid gap-2 p-4 lg:w-[500px] lg:grid-cols-[minmax(0,.75fr)_minmax(0,1fr)]">
|
<ul class="grid gap-2 p-4 lg:w-[500px] lg:grid-cols-[minmax(0,.75fr)_minmax(0,1fr)]">
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const open = ref(false)
|
||||||
|
const anchor = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
const reference = computed(() => ({
|
||||||
|
getBoundingClientRect: () =>
|
||||||
|
({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
left: anchor.value.x,
|
||||||
|
right: anchor.value.x,
|
||||||
|
top: anchor.value.y,
|
||||||
|
bottom: anchor.value.y,
|
||||||
|
...anchor.value
|
||||||
|
} as DOMRect)
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPopover
|
||||||
|
:open="open"
|
||||||
|
:reference="reference"
|
||||||
|
:content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
|
||||||
|
@pointerenter="open = true"
|
||||||
|
@pointerleave="open = false"
|
||||||
|
@pointermove="(ev) => {
|
||||||
|
anchor.x = ev.clientX
|
||||||
|
anchor.y = ev.clientY
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Hover me
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<div class="p-4">
|
||||||
|
{{ anchor.x.toFixed(0) }} - {{ anchor.y.toFixed(0) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UPopover>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { data: users } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
|
key: 'typicode-users-email',
|
||||||
|
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||||
|
return data?.map(user => ({
|
||||||
|
label: user.name,
|
||||||
|
value: String(user.id),
|
||||||
|
email: user.email,
|
||||||
|
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
lazy: true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelectMenu
|
||||||
|
:items="users"
|
||||||
|
icon="i-lucide-user"
|
||||||
|
placeholder="Select user"
|
||||||
|
:ui="{ content: 'min-w-fit' }"
|
||||||
|
class="w-48"
|
||||||
|
>
|
||||||
|
<template #item-label="{ item }">
|
||||||
|
{{ item.label }}
|
||||||
|
|
||||||
|
<span class="text-muted">
|
||||||
|
{{ item.email }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
</template>
|
||||||
@@ -35,6 +35,7 @@ const items = ref([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
] satisfies SelectMenuItem[])
|
] satisfies SelectMenuItem[])
|
||||||
|
|
||||||
const value = ref(items.value[0])
|
const value = ref(items.value[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const items = ref([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
] satisfies SelectMenuItem[])
|
] satisfies SelectMenuItem[])
|
||||||
|
|
||||||
const value = ref(items.value[0])
|
const value = ref(items.value[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const items = ref([
|
|||||||
icon: 'i-lucide-circle-check'
|
icon: 'i-lucide-circle-check'
|
||||||
}
|
}
|
||||||
] satisfies SelectMenuItem[])
|
] satisfies SelectMenuItem[])
|
||||||
|
|
||||||
const value = ref(items.value[0])
|
const value = ref(items.value[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const value = ref<string>()
|
||||||
|
|
||||||
|
const { data: users } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
|
key: 'typicode-users-email',
|
||||||
|
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||||
|
return data?.map(user => ({
|
||||||
|
label: user.name,
|
||||||
|
email: user.email,
|
||||||
|
value: String(user.id),
|
||||||
|
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
lazy: true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelect
|
||||||
|
v-model="value"
|
||||||
|
:items="users"
|
||||||
|
placeholder="Select user"
|
||||||
|
value-key="value"
|
||||||
|
:ui="{ content: 'min-w-fit' }"
|
||||||
|
class="w-48"
|
||||||
|
>
|
||||||
|
<template #item-label="{ item }">
|
||||||
|
{{ item.label }}
|
||||||
|
|
||||||
|
<span class="text-muted">
|
||||||
|
{{ item.email }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</USelect>
|
||||||
|
</template>
|
||||||
@@ -24,8 +24,8 @@ function getUserAvatar(value: string) {
|
|||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
icon="i-lucide-user"
|
icon="i-lucide-user"
|
||||||
placeholder="Select user"
|
placeholder="Select user"
|
||||||
class="w-48"
|
|
||||||
value-key="value"
|
value-key="value"
|
||||||
|
class="w-48"
|
||||||
>
|
>
|
||||||
<template #leading="{ modelValue, ui }">
|
<template #leading="{ modelValue, ui }">
|
||||||
<UAvatar
|
<UAvatar
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const items = ref([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
] satisfies SelectItem[])
|
] satisfies SelectItem[])
|
||||||
|
|
||||||
const value = ref(items.value[0]?.value)
|
const value = ref(items.value[0]?.value)
|
||||||
|
|
||||||
const avatar = computed(() => items.value.find(item => item.value === value.value)?.avatar)
|
const avatar = computed(() => items.value.find(item => item.value === value.value)?.avatar)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const items = ref([
|
|||||||
icon: 'i-lucide-circle-check'
|
icon: 'i-lucide-circle-check'
|
||||||
}
|
}
|
||||||
] satisfies SelectItem[])
|
] satisfies SelectItem[])
|
||||||
|
|
||||||
const value = ref(items.value[0]?.value)
|
const value = ref(items.value[0]?.value)
|
||||||
|
|
||||||
const icon = computed(() => items.value.find(item => item.value === value.value)?.icon)
|
const icon = computed(() => items.value.find(item => item.value === value.value)?.icon)
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { h, resolveComponent } from 'vue'
|
||||||
|
import type { TableColumn, TableRow } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const UBadge = resolveComponent('UBadge')
|
||||||
|
|
||||||
|
type Payment = {
|
||||||
|
id: string
|
||||||
|
date: string
|
||||||
|
status: 'paid' | 'failed' | 'refunded'
|
||||||
|
email: string
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = ref<Payment[]>([{
|
||||||
|
id: '4600',
|
||||||
|
date: '2024-03-11T15:30:00',
|
||||||
|
status: 'paid',
|
||||||
|
email: 'james.anderson@example.com',
|
||||||
|
amount: 594
|
||||||
|
}, {
|
||||||
|
id: '4599',
|
||||||
|
date: '2024-03-11T10:10:00',
|
||||||
|
status: 'failed',
|
||||||
|
email: 'mia.white@example.com',
|
||||||
|
amount: 276
|
||||||
|
}, {
|
||||||
|
id: '4598',
|
||||||
|
date: '2024-03-11T08:50:00',
|
||||||
|
status: 'refunded',
|
||||||
|
email: 'william.brown@example.com',
|
||||||
|
amount: 315
|
||||||
|
}, {
|
||||||
|
id: '4597',
|
||||||
|
date: '2024-03-10T19:45:00',
|
||||||
|
status: 'paid',
|
||||||
|
email: 'emma.davis@example.com',
|
||||||
|
amount: 529
|
||||||
|
}, {
|
||||||
|
id: '4596',
|
||||||
|
date: '2024-03-10T15:55:00',
|
||||||
|
status: 'paid',
|
||||||
|
email: 'ethan.harris@example.com',
|
||||||
|
amount: 639
|
||||||
|
}])
|
||||||
|
|
||||||
|
const columns: TableColumn<Payment>[] = [{
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: '#',
|
||||||
|
cell: ({ row }) => `#${row.getValue('id')}`
|
||||||
|
}, {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const color = ({
|
||||||
|
paid: 'success' as const,
|
||||||
|
failed: 'error' as const,
|
||||||
|
refunded: 'neutral' as const
|
||||||
|
})[row.getValue('status') as string]
|
||||||
|
|
||||||
|
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
accessorKey: 'email',
|
||||||
|
header: 'Email'
|
||||||
|
}, {
|
||||||
|
accessorKey: 'amount',
|
||||||
|
header: () => h('div', { class: 'text-right' }, 'Amount'),
|
||||||
|
footer: ({ column }) => {
|
||||||
|
const total = column.getFacetedRowModel().rows.reduce((acc: number, row: TableRow<Payment>) => acc + Number.parseFloat(row.getValue('amount')), 0)
|
||||||
|
|
||||||
|
const formatted = new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR'
|
||||||
|
}).format(total)
|
||||||
|
|
||||||
|
return h('div', { class: 'text-right font-medium' }, `Total: ${formatted}`)
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :data="data" :columns="columns" class="flex-1" />
|
||||||
|
</template>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { h, resolveComponent } from 'vue'
|
import { h, resolveComponent } from 'vue'
|
||||||
import { upperFirst } from 'scule'
|
import { upperFirst } from 'scule'
|
||||||
import type { TableColumn } from '@nuxt/ui'
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
|
||||||
const UButton = resolveComponent('UButton')
|
const UButton = resolveComponent('UButton')
|
||||||
const UCheckbox = resolveComponent('UCheckbox')
|
const UCheckbox = resolveComponent('UCheckbox')
|
||||||
@@ -9,6 +10,7 @@ const UBadge = resolveComponent('UBadge')
|
|||||||
const UDropdownMenu = resolveComponent('UDropdownMenu')
|
const UDropdownMenu = resolveComponent('UDropdownMenu')
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const { copy } = useClipboard()
|
||||||
|
|
||||||
type Payment = {
|
type Payment = {
|
||||||
id: string
|
id: string
|
||||||
@@ -220,7 +222,7 @@ const columns: TableColumn<Payment>[] = [{
|
|||||||
}, {
|
}, {
|
||||||
label: 'Copy payment ID',
|
label: 'Copy payment ID',
|
||||||
onSelect() {
|
onSelect() {
|
||||||
navigator.clipboard.writeText(row.original.id)
|
copy(row.original.id)
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'Payment ID copied to clipboard!',
|
title: 'Payment ID copied to clipboard!',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, resolveComponent } from 'vue'
|
import { h, resolveComponent } from 'vue'
|
||||||
import type { TableColumn } from '@nuxt/ui'
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
import { getGroupedRowModel, type GroupingOptions } from '@tanstack/vue-table'
|
import { getGroupedRowModel } from '@tanstack/vue-table'
|
||||||
|
import type { GroupingOptions } from '@tanstack/vue-table'
|
||||||
|
|
||||||
const UBadge = resolveComponent('UBadge')
|
const UBadge = resolveComponent('UBadge')
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
import { h, resolveComponent } from 'vue'
|
import { h, resolveComponent } from 'vue'
|
||||||
import type { TableColumn } from '@nuxt/ui'
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
import type { Row } from '@tanstack/vue-table'
|
import type { Row } from '@tanstack/vue-table'
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
|
||||||
const UButton = resolveComponent('UButton')
|
const UButton = resolveComponent('UButton')
|
||||||
const UBadge = resolveComponent('UBadge')
|
const UBadge = resolveComponent('UBadge')
|
||||||
const UDropdownMenu = resolveComponent('UDropdownMenu')
|
const UDropdownMenu = resolveComponent('UDropdownMenu')
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const { copy } = useClipboard()
|
||||||
|
|
||||||
type Payment = {
|
type Payment = {
|
||||||
id: string
|
id: string
|
||||||
@@ -119,7 +121,7 @@ function getRowItems(row: Row<Payment>) {
|
|||||||
}, {
|
}, {
|
||||||
label: 'Copy payment ID',
|
label: 'Copy payment ID',
|
||||||
onSelect() {
|
onSelect() {
|
||||||
navigator.clipboard.writeText(row.original.id)
|
copy(row.original.id)
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'Payment ID copied to clipboard!',
|
title: 'Payment ID copied to clipboard!',
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { h, resolveComponent } from 'vue'
|
||||||
|
import type { ContextMenuItem, TableColumn, TableRow } from '@nuxt/ui'
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
|
||||||
|
const UBadge = resolveComponent('UBadge')
|
||||||
|
const UCheckbox = resolveComponent('UCheckbox')
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const { copy } = useClipboard()
|
||||||
|
|
||||||
|
type Payment = {
|
||||||
|
id: string
|
||||||
|
date: string
|
||||||
|
status: 'paid' | 'failed' | 'refunded'
|
||||||
|
email: string
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = ref<Payment[]>([{
|
||||||
|
id: '4600',
|
||||||
|
date: '2024-03-11T15:30:00',
|
||||||
|
status: 'paid',
|
||||||
|
email: 'james.anderson@example.com',
|
||||||
|
amount: 594
|
||||||
|
}, {
|
||||||
|
id: '4599',
|
||||||
|
date: '2024-03-11T10:10:00',
|
||||||
|
status: 'failed',
|
||||||
|
email: 'mia.white@example.com',
|
||||||
|
amount: 276
|
||||||
|
}, {
|
||||||
|
id: '4598',
|
||||||
|
date: '2024-03-11T08:50:00',
|
||||||
|
status: 'refunded',
|
||||||
|
email: 'william.brown@example.com',
|
||||||
|
amount: 315
|
||||||
|
}, {
|
||||||
|
id: '4597',
|
||||||
|
date: '2024-03-10T19:45:00',
|
||||||
|
status: 'paid',
|
||||||
|
email: 'emma.davis@example.com',
|
||||||
|
amount: 529
|
||||||
|
}, {
|
||||||
|
id: '4596',
|
||||||
|
date: '2024-03-10T15:55:00',
|
||||||
|
status: 'paid',
|
||||||
|
email: 'ethan.harris@example.com',
|
||||||
|
amount: 639
|
||||||
|
}])
|
||||||
|
|
||||||
|
const columns: TableColumn<Payment>[] = [{
|
||||||
|
id: 'select',
|
||||||
|
header: ({ table }) => h(UCheckbox, {
|
||||||
|
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
|
||||||
|
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
|
||||||
|
'aria-label': 'Select all'
|
||||||
|
}),
|
||||||
|
cell: ({ row }) => h(UCheckbox, {
|
||||||
|
'modelValue': row.getIsSelected(),
|
||||||
|
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
|
||||||
|
'aria-label': 'Select row'
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: '#',
|
||||||
|
cell: ({ row }) => `#${row.getValue('id')}`
|
||||||
|
}, {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const color = ({
|
||||||
|
paid: 'success' as const,
|
||||||
|
failed: 'error' as const,
|
||||||
|
refunded: 'neutral' as const
|
||||||
|
})[row.getValue('status') as string]
|
||||||
|
|
||||||
|
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
accessorKey: 'email',
|
||||||
|
header: 'Email'
|
||||||
|
}, {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
const items = ref<ContextMenuItem[]>([])
|
||||||
|
|
||||||
|
function getRowItems(row: TableRow<Payment>) {
|
||||||
|
return [{
|
||||||
|
type: 'label' as const,
|
||||||
|
label: 'Actions'
|
||||||
|
}, {
|
||||||
|
label: 'Copy payment ID',
|
||||||
|
onSelect() {
|
||||||
|
copy(row.original.id)
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
title: 'Payment ID copied to clipboard!',
|
||||||
|
color: 'success',
|
||||||
|
icon: 'i-lucide-circle-check'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: row.getIsExpanded() ? 'Collapse' : 'Expand',
|
||||||
|
onSelect() {
|
||||||
|
row.toggleExpanded()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
type: 'separator' as const
|
||||||
|
}, {
|
||||||
|
label: 'View customer'
|
||||||
|
}, {
|
||||||
|
label: 'View payment details'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContextmenu(_e: Event, row: TableRow<Payment>) {
|
||||||
|
items.value = getRowItems(row)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UContextMenu :items="items">
|
||||||
|
<UTable
|
||||||
|
:data="data"
|
||||||
|
:columns="columns"
|
||||||
|
class="flex-1"
|
||||||
|
@contextmenu="onContextmenu"
|
||||||
|
>
|
||||||
|
<template #expanded="{ row }">
|
||||||
|
<pre>{{ row.original }}</pre>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</UContextMenu>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { h, resolveComponent } from 'vue'
|
||||||
|
import type { TableColumn, TableRow } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const UBadge = resolveComponent('UBadge')
|
||||||
|
const UCheckbox = resolveComponent('UCheckbox')
|
||||||
|
|
||||||
|
type Payment = {
|
||||||
|
id: string
|
||||||
|
date: string
|
||||||
|
status: 'paid' | 'failed' | 'refunded'
|
||||||
|
email: string
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = ref<Payment[]>([{
|
||||||
|
id: '4600',
|
||||||
|
date: '2024-03-11T15:30:00',
|
||||||
|
status: 'paid',
|
||||||
|
email: 'james.anderson@example.com',
|
||||||
|
amount: 594
|
||||||
|
}, {
|
||||||
|
id: '4599',
|
||||||
|
date: '2024-03-11T10:10:00',
|
||||||
|
status: 'failed',
|
||||||
|
email: 'mia.white@example.com',
|
||||||
|
amount: 276
|
||||||
|
}, {
|
||||||
|
id: '4598',
|
||||||
|
date: '2024-03-11T08:50:00',
|
||||||
|
status: 'refunded',
|
||||||
|
email: 'william.brown@example.com',
|
||||||
|
amount: 315
|
||||||
|
}, {
|
||||||
|
id: '4597',
|
||||||
|
date: '2024-03-10T19:45:00',
|
||||||
|
status: 'paid',
|
||||||
|
email: 'emma.davis@example.com',
|
||||||
|
amount: 529
|
||||||
|
}, {
|
||||||
|
id: '4596',
|
||||||
|
date: '2024-03-10T15:55:00',
|
||||||
|
status: 'paid',
|
||||||
|
email: 'ethan.harris@example.com',
|
||||||
|
amount: 639
|
||||||
|
}])
|
||||||
|
|
||||||
|
const columns: TableColumn<Payment>[] = [{
|
||||||
|
id: 'select',
|
||||||
|
header: ({ table }) => h(UCheckbox, {
|
||||||
|
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
|
||||||
|
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
|
||||||
|
'aria-label': 'Select all'
|
||||||
|
}),
|
||||||
|
cell: ({ row }) => h(UCheckbox, {
|
||||||
|
'modelValue': row.getIsSelected(),
|
||||||
|
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
|
||||||
|
'aria-label': 'Select row'
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: '#',
|
||||||
|
cell: ({ row }) => `#${row.getValue('id')}`
|
||||||
|
}, {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const color = ({
|
||||||
|
paid: 'success' as const,
|
||||||
|
failed: 'error' as const,
|
||||||
|
refunded: 'neutral' as const
|
||||||
|
})[row.getValue('status') as string]
|
||||||
|
|
||||||
|
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
accessorKey: 'email',
|
||||||
|
header: 'Email'
|
||||||
|
}, {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
const anchor = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
const reference = computed(() => ({
|
||||||
|
getBoundingClientRect: () =>
|
||||||
|
({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
left: anchor.value.x,
|
||||||
|
right: anchor.value.x,
|
||||||
|
top: anchor.value.y,
|
||||||
|
bottom: anchor.value.y,
|
||||||
|
...anchor.value
|
||||||
|
} as DOMRect)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const open = ref(false)
|
||||||
|
const openDebounced = refDebounced(open, 10)
|
||||||
|
const selectedRow = ref<TableRow<Payment> | null>(null)
|
||||||
|
|
||||||
|
function onHover(_e: Event, row: TableRow<Payment> | null) {
|
||||||
|
selectedRow.value = row
|
||||||
|
|
||||||
|
open.value = !!row
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex w-full flex-1 gap-1">
|
||||||
|
<UTable
|
||||||
|
:data="data"
|
||||||
|
:columns="columns"
|
||||||
|
class="flex-1"
|
||||||
|
@pointermove="(ev: PointerEvent) => {
|
||||||
|
anchor.x = ev.clientX
|
||||||
|
anchor.y = ev.clientY
|
||||||
|
}"
|
||||||
|
@hover="onHover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UPopover
|
||||||
|
:content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
|
||||||
|
:open="openDebounced"
|
||||||
|
:reference="reference"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="p-4">
|
||||||
|
{{ selectedRow?.original?.id }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UPopover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -112,7 +112,7 @@ function onSelect(row: TableRow<Payment>, e?: Event) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class=" flex w-full flex-1 gap-1">
|
<div class="flex w-full flex-1 gap-1">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<UTable
|
<UTable
|
||||||
ref="table"
|
ref="table"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
|
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number
|
id: number
|
||||||
@@ -10,6 +11,7 @@ interface User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const { copy } = useClipboard()
|
||||||
|
|
||||||
const data = ref<User[]>([{
|
const data = ref<User[]>([{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -71,7 +73,8 @@ function getDropdownActions(user: User): DropdownMenuItem[][] {
|
|||||||
label: 'Copy user Id',
|
label: 'Copy user Id',
|
||||||
icon: 'i-lucide-copy',
|
icon: 'i-lucide-copy',
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
navigator.clipboard.writeText(user.id.toString())
|
copy(user.id.toString())
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'User ID copied to clipboard!',
|
title: 'User ID copied to clipboard!',
|
||||||
color: 'success',
|
color: 'success',
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const state = reactive({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UTabs :items="items" variant="link" class="gap-4 w-full" :ui="{ trigger: 'grow' }">
|
<UTabs :items="items" variant="link" :ui="{ trigger: 'grow' }" class="gap-4 w-full">
|
||||||
<template #account="{ item }">
|
<template #account="{ item }">
|
||||||
<p class="text-muted mb-4">
|
<p class="text-muted mb-4">
|
||||||
{{ item.description }}
|
{{ item.description }}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ const items: TimelineItem[] = [{
|
|||||||
<template>
|
<template>
|
||||||
<UTimeline
|
<UTimeline
|
||||||
:items="items"
|
:items="items"
|
||||||
:ui="{ item: 'even:flex-row-reverse even:-translate-x-[calc(100%-2rem)] even:text-right' }"
|
|
||||||
:default-value="2"
|
:default-value="2"
|
||||||
|
:ui="{ item: 'even:flex-row-reverse even:-translate-x-[calc(100%-2rem)] even:text-right' }"
|
||||||
class="translate-x-[calc(50%-1rem)]"
|
class="translate-x-[calc(50%-1rem)]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ const items = [{
|
|||||||
<UTimeline
|
<UTimeline
|
||||||
:items="items"
|
:items="items"
|
||||||
size="xs"
|
size="xs"
|
||||||
class="w-96"
|
|
||||||
:ui="{
|
:ui="{
|
||||||
date: 'float-end ms-1',
|
date: 'float-end ms-1',
|
||||||
description: 'px-3 py-2 ring ring-default mt-2 rounded-md text-default'
|
description: 'px-3 py-2 ring ring-default mt-2 rounded-md text-default'
|
||||||
}"
|
}"
|
||||||
|
class="w-96"
|
||||||
>
|
>
|
||||||
<template #title="{ item }">
|
<template #title="{ item }">
|
||||||
<span>{{ item.username }}</span>
|
<span>{{ item.username }}</span>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
function showToast() {
|
||||||
|
toast.add({
|
||||||
|
title: 'Uh oh! Something went wrong.',
|
||||||
|
description: 'There was a problem with your request.',
|
||||||
|
icon: 'i-lucide-wifi',
|
||||||
|
progress: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
|
||||||
|
</template>
|
||||||
@@ -7,12 +7,12 @@ const appConfig = useAppConfig()
|
|||||||
<UFormField
|
<UFormField
|
||||||
label="toaster.duration"
|
label="toaster.duration"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="inline-flex ring ring-accented rounded-sm"
|
|
||||||
:ui="{
|
:ui="{
|
||||||
wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
|
wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
|
||||||
label: 'text-muted px-2 py-1.5',
|
label: 'text-muted px-2 py-1.5',
|
||||||
container: 'mt-0'
|
container: 'mt-0'
|
||||||
}"
|
}"
|
||||||
|
class="inline-flex ring ring-accented rounded-sm"
|
||||||
>
|
>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="appConfig.toaster.duration"
|
v-model="appConfig.toaster.duration"
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ const appConfig = useAppConfig()
|
|||||||
<UFormField
|
<UFormField
|
||||||
label="toaster.expand"
|
label="toaster.expand"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="inline-flex ring ring-accented rounded-sm"
|
|
||||||
:ui="{
|
:ui="{
|
||||||
wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
|
wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
|
||||||
label: 'text-muted px-2 py-1.5',
|
label: 'text-muted px-2 py-1.5',
|
||||||
container: 'mt-0'
|
container: 'mt-0'
|
||||||
}"
|
}"
|
||||||
|
class="inline-flex ring ring-accented rounded-sm"
|
||||||
>
|
>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model="appConfig.toaster.expand"
|
v-model="appConfig.toaster.expand"
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ const appConfig = useAppConfig()
|
|||||||
<UFormField
|
<UFormField
|
||||||
label="toaster.position"
|
label="toaster.position"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="inline-flex ring ring-accented rounded-sm"
|
|
||||||
:ui="{
|
:ui="{
|
||||||
wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
|
wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
|
||||||
label: 'text-muted px-2 py-1.5',
|
label: 'text-muted px-2 py-1.5',
|
||||||
container: 'mt-0'
|
container: 'mt-0'
|
||||||
}"
|
}"
|
||||||
|
class="inline-flex ring ring-accented rounded-sm"
|
||||||
>
|
>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model="appConfig.toaster.position"
|
v-model="appConfig.toaster.position"
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const open = ref(false)
|
||||||
|
const anchor = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
const reference = computed(() => ({
|
||||||
|
getBoundingClientRect: () =>
|
||||||
|
({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
left: anchor.value.x,
|
||||||
|
right: anchor.value.x,
|
||||||
|
top: anchor.value.y,
|
||||||
|
bottom: anchor.value.y,
|
||||||
|
...anchor.value
|
||||||
|
} as DOMRect)
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTooltip
|
||||||
|
:open="open"
|
||||||
|
:reference="reference"
|
||||||
|
:content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
|
||||||
|
@pointerenter="open = true"
|
||||||
|
@pointerleave="open = false"
|
||||||
|
@pointermove="(ev) => {
|
||||||
|
anchor.x = ev.clientX
|
||||||
|
anchor.y = ev.clientY
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Hover me
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
{{ anchor.x.toFixed(0) }} - {{ anchor.y.toFixed(0) }}
|
||||||
|
</template>
|
||||||
|
</UTooltip>
|
||||||
|
</template>
|
||||||
@@ -59,7 +59,7 @@ provide('navigation', mappedNavigation)
|
|||||||
<UApp>
|
<UApp>
|
||||||
<NuxtLoadingIndicator color="#FFF" />
|
<NuxtLoadingIndicator color="#FFF" />
|
||||||
|
|
||||||
<!-- <Banner /> -->
|
<Banner />
|
||||||
|
|
||||||
<Header :links="links" />
|
<Header :links="links" />
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import { kebabCase } from 'scule'
|
import { kebabCase } from 'scule'
|
||||||
import type { ContentNavigationItem } from '@nuxt/content'
|
import type { ContentNavigationItem } from '@nuxt/content'
|
||||||
import type { PageLink } from '@nuxt/ui-pro'
|
import type { PageLink } from '@nuxt/ui-pro'
|
||||||
import { findPageBreadcrumb, mapContentNavigation } from '@nuxt/ui-pro/utils/content'
|
import { mapContentNavigation } from '@nuxt/ui-pro/utils/content'
|
||||||
|
import { findPageBreadcrumb } from '@nuxt/content/utils'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { framework, module } = useSharedData()
|
const { framework, module } = useSharedData()
|
||||||
@@ -37,7 +38,7 @@ const { data: surround } = await useAsyncData(`${kebabCase(route.path)}-surround
|
|||||||
|
|
||||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
||||||
|
|
||||||
const breadcrumb = computed(() => mapContentNavigation(findPageBreadcrumb(navigation?.value, page.value)).map(({ icon, ...link }) => link))
|
const breadcrumb = computed(() => mapContentNavigation(findPageBreadcrumb(navigation?.value, page.value?.path, { indexAsChild: true })).map(({ icon, ...link }) => link))
|
||||||
|
|
||||||
if (!import.meta.prerender) {
|
if (!import.meta.prerender) {
|
||||||
// Redirect to the correct framework version if the page is not the current framework
|
// Redirect to the correct framework version if the page is not the current framework
|
||||||
@@ -141,7 +142,7 @@ const communityLinks = computed(() => [{
|
|||||||
<MDC v-if="page.description" :value="page.description" unwrap="p" :cache-key="`${kebabCase(route.path)}-description`" />
|
<MDC v-if="page.description" :value="page.description" unwrap="p" :cache-key="`${kebabCase(route.path)}-description`" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="page.links?.length" #links>
|
<template #links>
|
||||||
<UButton
|
<UButton
|
||||||
v-for="link in page.links"
|
v-for="link in page.links"
|
||||||
:key="link.label"
|
:key="link.label"
|
||||||
@@ -154,6 +155,7 @@ const communityLinks = computed(() => [{
|
|||||||
<UAvatar v-bind="link.avatar" size="2xs" :alt="`${link.label} avatar`" />
|
<UAvatar v-bind="link.avatar" size="2xs" :alt="`${link.label} avatar`" />
|
||||||
</template>
|
</template>
|
||||||
</UButton>
|
</UButton>
|
||||||
|
<PageHeaderLinks />
|
||||||
</template>
|
</template>
|
||||||
</UPageHeader>
|
</UPageHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ pricing:
|
|||||||
title: Upgrade to Nuxt UI [Pro]{class="text-primary"}.
|
title: Upgrade to Nuxt UI [Pro]{class="text-primary"}.
|
||||||
description: On top of 40+ open source components from Nuxt UI, Pro gives you access to 50+ premium Vue components to create beautiful & responsive Nuxt applications in minutes. It includes all primitives to build landing pages, documentations, blogs, dashboards or entire SaaS products.
|
description: On top of 40+ open source components from Nuxt UI, Pro gives you access to 50+ premium Vue components to create beautiful & responsive Nuxt applications in minutes. It includes all primitives to build landing pages, documentations, blogs, dashboards or entire SaaS products.
|
||||||
freePlan:
|
freePlan:
|
||||||
|
description: "**NuxtLabs is joining Vercel** :tada: As part of this transition, Nuxt UI is becoming even more accessible.<br><br> **In September, we're launching Nuxt UI v4**: a free, open-source library that unifies Nuxt UI and Nuxt UI Pro, offering 100+ components and a complete free Figma Kit for everyone."
|
||||||
|
orientation: horizontal
|
||||||
|
button:
|
||||||
|
label: Read the announcement
|
||||||
|
to: 'https://nuxtlabs.com/?utm_source=nuxt-ui&utm_medium=banner&utm_campaign=nuxtlabs-vercel'
|
||||||
|
target: _blank
|
||||||
|
color: 'neutral'
|
||||||
|
trailingIcon: 'i-lucide-arrow-right'
|
||||||
|
ui:
|
||||||
|
trailingIcon: 'ms-0'
|
||||||
|
devPlan:
|
||||||
title: Free in development
|
title: Free in development
|
||||||
description: Try Nuxt UI Pro for free in development, no credit card required. Upgrade when ready to deploy.
|
description: Try Nuxt UI Pro for free in development, no credit card required. Upgrade when ready to deploy.
|
||||||
orientation: horizontal
|
orientation: horizontal
|
||||||
@@ -13,6 +24,9 @@ pricing:
|
|||||||
to: '/getting-started/installation/pro/nuxt'
|
to: '/getting-started/installation/pro/nuxt'
|
||||||
color: 'neutral'
|
color: 'neutral'
|
||||||
variant: 'subtle'
|
variant: 'subtle'
|
||||||
|
trailingIcon: 'i-lucide-arrow-right'
|
||||||
|
ui:
|
||||||
|
trailingIcon: 'ms-0'
|
||||||
figma:
|
figma:
|
||||||
title: Figma Kit Pro
|
title: Figma Kit Pro
|
||||||
description: Get all Nuxt UI Pro components in a Figma kit to design your next application before coding. Everything you need, from wire-framing to high-fidelity web integration.
|
description: Get all Nuxt UI Pro components in a Figma kit to design your next application before coding. Everything you need, from wire-framing to high-fidelity web integration.
|
||||||
|
|||||||
@@ -34,10 +34,19 @@ useSeoMeta({
|
|||||||
<div class="flex flex-col bg-default gap-8 lg:gap-0">
|
<div class="flex flex-col bg-default gap-8 lg:gap-0">
|
||||||
<UPricingPlan
|
<UPricingPlan
|
||||||
v-bind="page.pricing.freePlan"
|
v-bind="page.pricing.freePlan"
|
||||||
variant="naked"
|
class="lg:rounded-none ring-primary/15 ring-inset -mb-px bg-primary/5 z-[1]"
|
||||||
class="lg:rounded-none border-x border-default border-t border-b lg:border-b-0"
|
:ui="{ description: 'mt-0 text-primary' }"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
<MDC :value="page.pricing.freePlan.description" unwrap="p" />
|
||||||
|
</template>
|
||||||
|
</UPricingPlan>
|
||||||
|
|
||||||
|
<UPricingPlan
|
||||||
|
v-bind="page.pricing.devPlan"
|
||||||
|
class="lg:rounded-none ring-inset -mb-px"
|
||||||
/>
|
/>
|
||||||
<UPricingPlans compact>
|
<UPricingPlans compact class="-space-x-px">
|
||||||
<UPricingPlan
|
<UPricingPlan
|
||||||
v-for="(plan, index) in page.pricing.plans"
|
v-for="(plan, index) in page.pricing.plans"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -47,18 +56,17 @@ useSeoMeta({
|
|||||||
:discount="plan.discount"
|
:discount="plan.discount"
|
||||||
:billing-period="plan.billing_period"
|
:billing-period="plan.billing_period"
|
||||||
:billing-cycle="plan.billing_cycle"
|
:billing-cycle="plan.billing_cycle"
|
||||||
:variant="plan.highlight ? 'soft' : 'outline'"
|
:variant="plan.highlight ? 'subtle' : 'outline'"
|
||||||
:class="['lg:rounded-none', { 'border-2 lg:border lg:border-x-0 border-primary lg:border-default': plan.highlight }]"
|
class="lg:rounded-none ring-inset -mb-px"
|
||||||
:features="plan.features"
|
:features="plan.features"
|
||||||
:button="plan.button"
|
:button="plan.button"
|
||||||
/>
|
/>
|
||||||
</UPricingPlans>
|
</UPricingPlans>
|
||||||
<UPricingPlan
|
<UPricingPlan
|
||||||
v-bind="page.pricing.figma"
|
v-bind="page.pricing.figma"
|
||||||
variant="naked"
|
|
||||||
:billing-period="page.pricing.figma.billing_period"
|
:billing-period="page.pricing.figma.billing_period"
|
||||||
:billing-cycle="page.pricing.figma.billing_cycle"
|
:billing-cycle="page.pricing.figma.billing_cycle"
|
||||||
class="lg:rounded-none border lg:border-y-0 border-default"
|
class="lg:rounded-none ring-inset -mb-px"
|
||||||
>
|
>
|
||||||
<template #features>
|
<template #features>
|
||||||
<li v-for="(feature, index) in page.pricing.figma.features" :key="index" class="flex items-center gap-2 min-w-0">
|
<li v-for="(feature, index) in page.pricing.figma.features" :key="index" class="flex items-center gap-2 min-w-0">
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ function handleMessage(message) {
|
|||||||
async function handleFormatMessage(message) {
|
async function handleFormatMessage(message) {
|
||||||
if (!globalThis.prettier) {
|
if (!globalThis.prettier) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/standalone.js'),
|
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/standalone.js'),
|
||||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/babel.js'),
|
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/babel.js'),
|
||||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/estree.js'),
|
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/estree.js'),
|
||||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/html.js'),
|
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/html.js'),
|
||||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/markdown.js'),
|
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/markdown.js'),
|
||||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/typescript.js')
|
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/typescript.js')
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -225,6 +225,27 @@ export default defineNuxtConfig({
|
|||||||
This option adds the `transition-colors` class on components with hover or active states.
|
This option adds the `transition-colors` class on components with hover or active states.
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### `theme.defaultVariants` :badge{label="Soon" class="align-text-top"}
|
||||||
|
|
||||||
|
Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components.
|
||||||
|
|
||||||
|
- Default: `{ color: 'primary', size: 'md' }`{lang="ts-type"}
|
||||||
|
|
||||||
|
```ts [nuxt.config.ts]
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
modules: ['@nuxt/ui'],
|
||||||
|
css: ['~/assets/css/main.css'],
|
||||||
|
ui: {
|
||||||
|
theme: {
|
||||||
|
defaultVariants: {
|
||||||
|
color: 'neutral',
|
||||||
|
size: 'sm'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
## Continuous Releases
|
## Continuous Releases
|
||||||
|
|
||||||
Nuxt UI uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.
|
Nuxt UI uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.
|
||||||
|
|||||||
@@ -333,6 +333,32 @@ export default defineConfig({
|
|||||||
This option adds the `transition-colors` class on components with hover or active states.
|
This option adds the `transition-colors` class on components with hover or active states.
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### `theme.defaultVariants` :badge{label="Soon" class="align-text-top"}
|
||||||
|
|
||||||
|
Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components.
|
||||||
|
|
||||||
|
- Default: `{ color: 'primary', size: 'md' }`{lang="ts-type"}
|
||||||
|
|
||||||
|
```ts [vite.config.ts]
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import ui from '@nuxt/ui/vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
ui({
|
||||||
|
theme: {
|
||||||
|
defaultVariants: {
|
||||||
|
color: 'neutral',
|
||||||
|
size: 'sm'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
### `inertia`
|
### `inertia`
|
||||||
|
|
||||||
Use the `inertia` option to enable compatibility with [Inertia.js](https://inertiajs.com/).
|
Use the `inertia` option to enable compatibility with [Inertia.js](https://inertiajs.com/).
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ props:
|
|||||||
You can use any name from the <https://icones.js.org> collection.
|
You can use any name from the <https://icones.js.org> collection.
|
||||||
::
|
::
|
||||||
|
|
||||||
|
::warning
|
||||||
|
When using collections with a dash (`-`), you need to separate the icon name from the collection name with a colon (`:`) as `@iconify/vue` does not handle this case like `@nuxt/icon`. For example, instead of `i-simple-icons-github` you need to write `i-simple-icons:github` or `simple-icons:github`.
|
||||||
|
|
||||||
|
Learn more about the [Iconify naming convention](https://iconify.design/docs/icon-components/vue/#icon).
|
||||||
|
::
|
||||||
|
|
||||||
### Component Props
|
### Component Props
|
||||||
|
|
||||||
Some components also have an `icon` prop to display an icon, like the [Button](/components/button) for example:
|
Some components also have an `icon` prop to display an icon, like the [Button](/components/button) for example:
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ Look at the `code` parameter, there you need to pass the iso code of the languag
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
### Extend locale :badge{label="Soon" class="align-text-top"}
|
### Extend locale :badge{label="New" class="align-text-top"}
|
||||||
|
|
||||||
You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:
|
You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ Look at the `code` parameter, there you need to pass the iso code of the languag
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
### Extend locale :badge{label="Soon" class="align-text-top"}
|
### Extend locale :badge{label="New" class="align-text-top"}
|
||||||
|
|
||||||
You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:
|
You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ links:
|
|||||||
- label: GitHub
|
- label: GitHub
|
||||||
icon: i-simple-icons-github
|
icon: i-simple-icons-github
|
||||||
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/CheckboxGroup.vue
|
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/CheckboxGroup.vue
|
||||||
navigation.badge: New
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### Variant :badge{label="New" class="align-text-top"}
|
### Variant
|
||||||
|
|
||||||
Use the `variant` prop to change the variant of the Checkbox.
|
Use the `variant` prop to change the variant of the Checkbox.
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### Indicator :badge{label="New" class="align-text-top"}
|
### Indicator
|
||||||
|
|
||||||
Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.
|
Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.
|
||||||
|
|
||||||
|
|||||||
@@ -52,9 +52,11 @@ Each group contains an `items` array of objects that define the commands. Each i
|
|||||||
- `loading?: boolean`{lang="ts-type"}
|
- `loading?: boolean`{lang="ts-type"}
|
||||||
- `disabled?: boolean`{lang="ts-type"}
|
- `disabled?: boolean`{lang="ts-type"}
|
||||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||||
|
- `placeholder?: string`{lang="ts-type"}
|
||||||
|
- `children?: CommandPaletteItem[]`{lang="ts-type"}
|
||||||
- `onSelect?(e?: Event): void`{lang="ts-type"}
|
- `onSelect?(e?: Event): void`{lang="ts-type"}
|
||||||
- `class?: any`{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"}
|
- `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.
|
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
|
||||||
|
|
||||||
@@ -110,6 +112,10 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
::tip{to="#with-children-in-items"}
|
||||||
|
Each item can take a `children` array of objects with the following properties to create submenus:
|
||||||
|
::
|
||||||
|
|
||||||
### Multiple
|
### Multiple
|
||||||
|
|
||||||
Use the `multiple` prop to allow multiple selections.
|
Use the `multiple` prop to allow multiple selections.
|
||||||
@@ -246,6 +252,128 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.se
|
|||||||
:::
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Selected Icon
|
||||||
|
|
||||||
|
Use the `selected-icon` prop to customize the selected item [Icon](/components/icon). Defaults to `i-lucide-check`.
|
||||||
|
|
||||||
|
::component-code
|
||||||
|
---
|
||||||
|
collapse: true
|
||||||
|
hide:
|
||||||
|
- autofocus
|
||||||
|
ignore:
|
||||||
|
- groups
|
||||||
|
- modelValue
|
||||||
|
- multiple
|
||||||
|
- class
|
||||||
|
external:
|
||||||
|
- groups
|
||||||
|
- modelValue
|
||||||
|
class: '!p-0'
|
||||||
|
props:
|
||||||
|
multiple: true
|
||||||
|
autofocus: false
|
||||||
|
modelValue:
|
||||||
|
- label: 'Benjamin Canac'
|
||||||
|
suffix: 'benjamincanac'
|
||||||
|
avatar:
|
||||||
|
src: 'https://github.com/benjamincanac.png'
|
||||||
|
selectedIcon: 'i-lucide-circle-check'
|
||||||
|
groups:
|
||||||
|
- id: 'users'
|
||||||
|
label: 'Users'
|
||||||
|
items:
|
||||||
|
- label: 'Benjamin Canac'
|
||||||
|
suffix: 'benjamincanac'
|
||||||
|
avatar:
|
||||||
|
src: 'https://github.com/benjamincanac.png'
|
||||||
|
- label: 'Sylvain Marroufin'
|
||||||
|
suffix: 'smarroufin'
|
||||||
|
avatar:
|
||||||
|
src: 'https://github.com/smarroufin.png'
|
||||||
|
- label: 'Sébastien Chopin'
|
||||||
|
suffix: 'atinux'
|
||||||
|
avatar:
|
||||||
|
src: 'https://github.com/atinux.png'
|
||||||
|
- label: 'Romain Hamel'
|
||||||
|
suffix: 'romhml'
|
||||||
|
avatar:
|
||||||
|
src: 'https://github.com/romhml.png'
|
||||||
|
- label: 'Haytham A. Salama'
|
||||||
|
suffix: 'Haythamasalama'
|
||||||
|
avatar:
|
||||||
|
src: 'https://github.com/Haythamasalama.png'
|
||||||
|
- label: 'Daniel Roe'
|
||||||
|
suffix: 'danielroe'
|
||||||
|
avatar:
|
||||||
|
src: 'https://github.com/danielroe.png'
|
||||||
|
- label: 'Neil Richter'
|
||||||
|
suffix: 'noook'
|
||||||
|
avatar:
|
||||||
|
src: 'https://github.com/noook.png'
|
||||||
|
class: 'flex-1'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
::framework-only
|
||||||
|
#nuxt
|
||||||
|
:::tip{to="/getting-started/icons/nuxt#theme"}
|
||||||
|
You can customize this icon globally in your `app.config.ts` under `ui.icons.check` key.
|
||||||
|
:::
|
||||||
|
|
||||||
|
#vue
|
||||||
|
:::tip{to="/getting-started/icons/vue#theme"}
|
||||||
|
You can customize this icon globally in your `vite.config.ts` under `ui.icons.check` key.
|
||||||
|
:::
|
||||||
|
::
|
||||||
|
|
||||||
|
### Trailing Icon :badge{label="New" class="align-text-top"}
|
||||||
|
|
||||||
|
Use the `trailing-icon` prop to customize the trailing [Icon](/components/icon) when an item has children. Defaults to `i-lucide-chevron-right`.
|
||||||
|
|
||||||
|
::component-code
|
||||||
|
---
|
||||||
|
collapse: true
|
||||||
|
prettier: true
|
||||||
|
hide:
|
||||||
|
- autofocus
|
||||||
|
ignore:
|
||||||
|
- groups
|
||||||
|
- class
|
||||||
|
external:
|
||||||
|
- groups
|
||||||
|
class: '!p-0'
|
||||||
|
props:
|
||||||
|
autofocus: false
|
||||||
|
trailingIcon: 'i-lucide-arrow-right'
|
||||||
|
groups:
|
||||||
|
- id: 'actions'
|
||||||
|
items:
|
||||||
|
- label: 'Share'
|
||||||
|
icon: 'i-lucide-share'
|
||||||
|
children:
|
||||||
|
- label: 'Email'
|
||||||
|
icon: 'i-lucide-mail'
|
||||||
|
- label: 'Copy'
|
||||||
|
icon: 'i-lucide-copy'
|
||||||
|
- label: 'Link'
|
||||||
|
icon: 'i-lucide-link'
|
||||||
|
class: 'flex-1'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
::framework-only
|
||||||
|
#nuxt
|
||||||
|
:::tip{to="/getting-started/icons/nuxt#theme"}
|
||||||
|
You can customize this icon globally in your `app.config.ts` under `ui.icons.chevronRight` key.
|
||||||
|
:::
|
||||||
|
|
||||||
|
#vue
|
||||||
|
:::tip{to="/getting-started/icons/vue#theme"}
|
||||||
|
You can customize this icon globally in your `vite.config.ts` under `ui.icons.chevronRight` key.
|
||||||
|
:::
|
||||||
|
::
|
||||||
|
|
||||||
### Loading
|
### Loading
|
||||||
|
|
||||||
Use the `loading` prop to show a loading icon on the CommandPalette.
|
Use the `loading` prop to show a loading icon on the CommandPalette.
|
||||||
@@ -321,37 +449,6 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.lo
|
|||||||
:::
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Disabled
|
|
||||||
|
|
||||||
Use the `disabled` prop to disable the CommandPalette.
|
|
||||||
|
|
||||||
::component-code
|
|
||||||
---
|
|
||||||
collapse: true
|
|
||||||
hide:
|
|
||||||
- autofocus
|
|
||||||
ignore:
|
|
||||||
- groups
|
|
||||||
- class
|
|
||||||
external:
|
|
||||||
- groups
|
|
||||||
class: '!p-0'
|
|
||||||
props:
|
|
||||||
autofocus: false
|
|
||||||
disabled: true
|
|
||||||
groups:
|
|
||||||
- id: 'apps'
|
|
||||||
items:
|
|
||||||
- label: 'Calendar'
|
|
||||||
icon: 'i-lucide-calendar'
|
|
||||||
- label: 'Music'
|
|
||||||
icon: 'i-lucide-music'
|
|
||||||
- label: 'Maps'
|
|
||||||
icon: 'i-lucide-map'
|
|
||||||
class: 'flex-1'
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
### Close
|
### Close
|
||||||
|
|
||||||
Use the `close` prop to display a [Button](/components/button) to dismiss the CommandPalette.
|
Use the `close` prop to display a [Button](/components/button) to dismiss the CommandPalette.
|
||||||
@@ -468,6 +565,124 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.cl
|
|||||||
:::
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Back :badge{label="New" class="align-text-top"}
|
||||||
|
|
||||||
|
Use the `back` prop to customize or hide the back button (with `false` value) displayed when navigating into a submenu.
|
||||||
|
|
||||||
|
You can pass any property from the [Button](/components/button) component to customize it.
|
||||||
|
|
||||||
|
::component-code
|
||||||
|
---
|
||||||
|
collapse: true
|
||||||
|
prettier: true
|
||||||
|
hide:
|
||||||
|
- autofocus
|
||||||
|
ignore:
|
||||||
|
- back.color
|
||||||
|
- groups
|
||||||
|
- class
|
||||||
|
external:
|
||||||
|
- groups
|
||||||
|
class: '!p-0'
|
||||||
|
props:
|
||||||
|
autofocus: false
|
||||||
|
back:
|
||||||
|
color: primary
|
||||||
|
groups:
|
||||||
|
- id: 'actions'
|
||||||
|
items:
|
||||||
|
- label: 'Share'
|
||||||
|
icon: 'i-lucide-share'
|
||||||
|
children:
|
||||||
|
- label: 'Email'
|
||||||
|
icon: 'i-lucide-mail'
|
||||||
|
- label: 'Copy'
|
||||||
|
icon: 'i-lucide-copy'
|
||||||
|
- label: 'Link'
|
||||||
|
icon: 'i-lucide-link'
|
||||||
|
class: 'flex-1'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Back Icon :badge{label="New" class="align-text-top"}
|
||||||
|
|
||||||
|
Use the `back-icon` prop to customize the back button [Icon](/components/icon). Defaults to `i-lucide-arrow-left`.
|
||||||
|
|
||||||
|
::component-code
|
||||||
|
---
|
||||||
|
collapse: true
|
||||||
|
hide:
|
||||||
|
- autofocus
|
||||||
|
ignore:
|
||||||
|
- class
|
||||||
|
- groups
|
||||||
|
- back
|
||||||
|
external:
|
||||||
|
- groups
|
||||||
|
class: '!p-0'
|
||||||
|
props:
|
||||||
|
autofocus: false
|
||||||
|
back: true
|
||||||
|
backIcon: 'i-lucide-house'
|
||||||
|
groups:
|
||||||
|
- id: 'actions'
|
||||||
|
items:
|
||||||
|
- label: 'Share'
|
||||||
|
icon: 'i-lucide-share'
|
||||||
|
children:
|
||||||
|
- label: 'Email'
|
||||||
|
icon: 'i-lucide-mail'
|
||||||
|
- label: 'Copy'
|
||||||
|
icon: 'i-lucide-copy'
|
||||||
|
- label: 'Link'
|
||||||
|
icon: 'i-lucide-link'
|
||||||
|
class: 'flex-1'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
::framework-only
|
||||||
|
#nuxt
|
||||||
|
:::tip{to="/getting-started/icons/nuxt#theme"}
|
||||||
|
You can customize this icon globally in your `app.config.ts` under `ui.icons.arrowLeft` key.
|
||||||
|
:::
|
||||||
|
|
||||||
|
#vue
|
||||||
|
:::tip{to="/getting-started/icons/vue#theme"}
|
||||||
|
You can customize this icon globally in your `vite.config.ts` under `ui.icons.arrowLeft` key.
|
||||||
|
:::
|
||||||
|
::
|
||||||
|
|
||||||
|
### Disabled
|
||||||
|
|
||||||
|
Use the `disabled` prop to disable the CommandPalette.
|
||||||
|
|
||||||
|
::component-code
|
||||||
|
---
|
||||||
|
collapse: true
|
||||||
|
hide:
|
||||||
|
- autofocus
|
||||||
|
ignore:
|
||||||
|
- groups
|
||||||
|
- class
|
||||||
|
external:
|
||||||
|
- groups
|
||||||
|
class: '!p-0'
|
||||||
|
props:
|
||||||
|
autofocus: false
|
||||||
|
disabled: true
|
||||||
|
groups:
|
||||||
|
- id: 'apps'
|
||||||
|
items:
|
||||||
|
- label: 'Calendar'
|
||||||
|
icon: 'i-lucide-calendar'
|
||||||
|
- label: 'Music'
|
||||||
|
icon: 'i-lucide-music'
|
||||||
|
- label: 'Maps'
|
||||||
|
icon: 'i-lucide-map'
|
||||||
|
class: 'flex-1'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Control selected item(s)
|
### Control selected item(s)
|
||||||
@@ -502,6 +717,28 @@ props:
|
|||||||
This example uses the `@update:model-value` event to reset the search term when an item is selected.
|
This example uses the `@update:model-value` event to reset the search term when an item is selected.
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### With children in items :badge{label="New" class="align-text-top"}
|
||||||
|
|
||||||
|
You can create hierarchical menus by using the `children` property in items. When an item has children, it will automatically display a chevron icon and enable navigation into a submenu.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
collapse: true
|
||||||
|
prettier: true
|
||||||
|
name: 'command-palette-items-children-example'
|
||||||
|
class: '!p-0'
|
||||||
|
props:
|
||||||
|
autofocus: false
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
::note
|
||||||
|
When navigating into a submenu:
|
||||||
|
- The search term is reset
|
||||||
|
- A back button appears in the input
|
||||||
|
- You can go back to the previous group by pressing the :kbd{value="backspace"} key
|
||||||
|
::
|
||||||
|
|
||||||
### With fetched items
|
### With fetched items
|
||||||
|
|
||||||
You can fetch items from an API and use them in the CommandPalette.
|
You can fetch items from an API and use them in the CommandPalette.
|
||||||
@@ -640,6 +877,20 @@ props:
|
|||||||
This can be useful when using the CommandPalette inside a [`Modal`](/components/modal) for example.
|
This can be useful when using the CommandPalette inside a [`Modal`](/components/modal) for example.
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### With footer slot :badge{label="Soon" class="align-text-top"}
|
||||||
|
|
||||||
|
Use the `#footer` slot to add custom content at the bottom of the CommandPalette, such as keyboard shortcuts help or additional actions.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
collapse: true
|
||||||
|
name: 'command-palette-footer-slot-example'
|
||||||
|
class: '!p-0'
|
||||||
|
props:
|
||||||
|
autofocus: false
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
### With custom slot
|
### With custom slot
|
||||||
|
|
||||||
Use the `slot` property to customize a specific item or group.
|
Use the `slot` property to customize a specific item or group.
|
||||||
@@ -658,6 +909,7 @@ You will have access to the following slots:
|
|||||||
|
|
||||||
::component-example
|
::component-example
|
||||||
---
|
---
|
||||||
|
collapse: true
|
||||||
name: 'command-palette-custom-slot-example'
|
name: 'command-palette-custom-slot-example'
|
||||||
class: '!p-0'
|
class: '!p-0'
|
||||||
props:
|
props:
|
||||||
|
|||||||
@@ -757,6 +757,33 @@ name: 'input-menu-filter-fields-example'
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### With full content width
|
||||||
|
|
||||||
|
You can expand the content to the full width of its items by using the `ui.content` key.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
name: 'input-menu-content-width-example'
|
||||||
|
collapse: true
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
::tip
|
||||||
|
You can also change the content width globally in your `app.config.ts`:
|
||||||
|
|
||||||
|
```
|
||||||
|
export default defineAppConfig({
|
||||||
|
ui: {
|
||||||
|
inputMenu: {
|
||||||
|
slots: {
|
||||||
|
content: 'min-w-fit'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
### As a CountryPicker
|
### As a CountryPicker
|
||||||
|
|
||||||
This example demonstrates using the InputMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
|
This example demonstrates using the InputMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
|
||||||
@@ -782,6 +809,14 @@ name: 'input-menu-countries-example'
|
|||||||
|
|
||||||
:component-emits
|
:component-emits
|
||||||
|
|
||||||
|
### Expose
|
||||||
|
|
||||||
|
When accessing the component via a template ref, you can use the following:
|
||||||
|
|
||||||
|
| Name | Type |
|
||||||
|
| ---- | ---- |
|
||||||
|
| `inputRef`{lang="ts-type"} | `Ref<InstanceType<typeof ComboboxTrigger> \| null>`{lang="ts-type"} |
|
||||||
|
|
||||||
## Theme
|
## Theme
|
||||||
|
|
||||||
:component-theme
|
:component-theme
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: InputNumber
|
title: InputNumber
|
||||||
description: Input numerical values with a customizable range.
|
description: An input for numerical values with a customizable range.
|
||||||
category: form
|
category: form
|
||||||
links:
|
links:
|
||||||
- label: NumberField
|
- label: NumberField
|
||||||
@@ -287,8 +287,8 @@ name: 'input-number-slots-example'
|
|||||||
When accessing the component via a template ref, you can use the following:
|
When accessing the component via a template ref, you can use the following:
|
||||||
|
|
||||||
| Name | Type |
|
| Name | Type |
|
||||||
|----------------------------|-------------------------------------------------|
|
| -------------------------- | ----------------------------------------------- |
|
||||||
| `inputRef`{lang="ts-type"} | `Ref<HTMLInputElement \| null>`{lang="ts-type"} |
|
| `inputRef`{lang="ts-type"} | `Ref<InstanceType<typeof NumberFieldInput> \| null>`{lang="ts-type"} |
|
||||||
|
|
||||||
## Theme
|
## Theme
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: InputTags
|
title: InputTags
|
||||||
description: An input element that displays interactive tags.
|
description: An input element that displays interactive tags.
|
||||||
|
category: form
|
||||||
links:
|
links:
|
||||||
- label: InputTags
|
- label: InputTags
|
||||||
icon: i-custom-reka-ui
|
icon: i-custom-reka-ui
|
||||||
@@ -8,7 +9,7 @@ links:
|
|||||||
- label: GitHub
|
- label: GitHub
|
||||||
icon: i-simple-icons-github
|
icon: i-simple-icons-github
|
||||||
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/InputTags.vue
|
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/InputTags.vue
|
||||||
navigation.badge: Soon
|
navigation.badge: New
|
||||||
---
|
---
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -50,6 +51,17 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Max Length :badge{label="Soon" class="align-text-top"}
|
||||||
|
|
||||||
|
Use the `max-length` prop to set the maximum number of characters allowed in a tag.
|
||||||
|
|
||||||
|
::component-code
|
||||||
|
---
|
||||||
|
props:
|
||||||
|
maxLength: 4
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
### Color
|
### Color
|
||||||
|
|
||||||
Use the `color` prop to change the ring color when the InputTags is focused.
|
Use the `color` prop to change the ring color when the InputTags is focused.
|
||||||
@@ -276,8 +288,8 @@ name: 'input-tags-form-field-example'
|
|||||||
When accessing the component via a template ref, you can use the following:
|
When accessing the component via a template ref, you can use the following:
|
||||||
|
|
||||||
| Name | Type |
|
| Name | Type |
|
||||||
|----------------------------|-------------------------------------------------|
|
| -------------------------- | ----------------------------------------------- |
|
||||||
| `inputRef`{lang="ts-type"} | `Ref<HTMLInputElement \| null>`{lang="ts-type"} |
|
| `inputRef`{lang="ts-type"} | `Ref<InstanceType<typeof TagsInputInput> \| null>`{lang="ts-type"} |
|
||||||
|
|
||||||
## Theme
|
## Theme
|
||||||
|
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ This example uses the `defineShortcuts` composable to focus the Input when the :
|
|||||||
|
|
||||||
### With mask
|
### With mask
|
||||||
|
|
||||||
There's no built-in support for masks, but you can use librairies like [maska](https://github.com/beholdr/maska) to mask the Input.
|
There's no built-in support for masks, but you can use libraries like [maska](https://github.com/beholdr/maska) to mask the Input.
|
||||||
|
|
||||||
::component-example
|
::component-example
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -889,7 +889,7 @@ You can inspect the DOM to see each item's content being rendered.
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### With tooltip in items :badge{label="New" class="align-text-top"}
|
### With tooltip in items
|
||||||
|
|
||||||
When orientation is `vertical` and the menu is `collapsed`, you can set the `tooltip` prop to `true` to display a [Tooltip](/components/tooltip) around items with their label but you can also use the `tooltip` property on each item to override the default tooltip.
|
When orientation is `vertical` and the menu is `collapsed`, you can set the `tooltip` prop to `true` to display a [Tooltip](/components/tooltip) around items with their label but you can also use the `tooltip` property on each item to override the default tooltip.
|
||||||
|
|
||||||
@@ -994,7 +994,7 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### With popover in items :badge{label="New" class="align-text-top"}
|
### With popover in items
|
||||||
|
|
||||||
When orientation is `vertical` and the menu is `collapsed`, you can set the `popover` prop to `true` to display a [Popover](/components/popover) around items with their children but you can also use the `popover` property on each item to override the default popover.
|
When orientation is `vertical` and the menu is `collapsed`, you can set the `popover` prop to `true` to display a [Popover](/components/popover) around items with their children but you can also use the `popover` property on each item to override the default popover.
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ props:
|
|||||||
|
|
||||||
:component-emits
|
:component-emits
|
||||||
|
|
||||||
|
### Expose
|
||||||
|
|
||||||
When accessing the component via a template ref, you can use the following:
|
When accessing the component via a template ref, you can use the following:
|
||||||
|
|
||||||
| Name | Type |
|
| Name | Type |
|
||||||
|
|||||||
@@ -202,7 +202,17 @@ name: 'popover-command-palette-example'
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### With anchor slot :badge{label="New" class="align-text-top"}
|
### With following cursor :badge{label="Soon" class="align-text-top"}
|
||||||
|
|
||||||
|
You can make the Popover follow the cursor when hovering over an element using the [`reference`](https://reka-ui.com/docs/components/tooltip#trigger) prop:
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
name: 'popover-cursor-example'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### With anchor slot
|
||||||
|
|
||||||
You can use the `#anchor` slot to position the Popover against a custom element.
|
You can use the `#anchor` slot to position the Popover against a custom element.
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### Variant :badge{label="New" class="align-text-top"}
|
### Variant
|
||||||
|
|
||||||
Use the `variant` prop to change the variant of the RadioGroup.
|
Use the `variant` prop to change the variant of the RadioGroup.
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### Indicator :badge{label="New" class="align-text-top"}
|
### Indicator
|
||||||
|
|
||||||
Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.
|
Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.
|
||||||
|
|
||||||
|
|||||||
@@ -790,6 +790,33 @@ name: 'select-menu-filter-fields-example'
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### With full content width
|
||||||
|
|
||||||
|
You can expand the content to the full width of its items by using the `ui.content` key.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
name: 'select-menu-content-width-example'
|
||||||
|
collapse: true
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
::tip
|
||||||
|
You can also change the content width globally in your `app.config.ts`:
|
||||||
|
|
||||||
|
```
|
||||||
|
export default defineAppConfig({
|
||||||
|
ui: {
|
||||||
|
selectMenu: {
|
||||||
|
slots: {
|
||||||
|
content: 'min-w-fit'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
### As a CountryPicker
|
### As a CountryPicker
|
||||||
|
|
||||||
This example demonstrates using the SelectMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
|
This example demonstrates using the SelectMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
|
||||||
@@ -801,6 +828,8 @@ name: 'select-menu-countries-example'
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
@@ -815,6 +844,14 @@ name: 'select-menu-countries-example'
|
|||||||
|
|
||||||
:component-emits
|
:component-emits
|
||||||
|
|
||||||
|
### Expose
|
||||||
|
|
||||||
|
When accessing the component via a template ref, you can use the following:
|
||||||
|
|
||||||
|
| Name | Type |
|
||||||
|
| ---- | ---- |
|
||||||
|
| `triggerRef`{lang="ts-type"} | `Ref<InstanceType<typeof ComboboxTrigger> \| null>`{lang="ts-type"} |
|
||||||
|
|
||||||
## Theme
|
## Theme
|
||||||
|
|
||||||
:component-theme
|
:component-theme
|
||||||
|
|||||||
@@ -695,6 +695,33 @@ collapse: true
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### With full content width
|
||||||
|
|
||||||
|
You can expand the content to the full width of its items by using the `ui.content` key.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
name: 'select-content-width-example'
|
||||||
|
collapse: true
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
::tip
|
||||||
|
You can also change the content width globally in your `app.config.ts`:
|
||||||
|
|
||||||
|
```
|
||||||
|
export default defineAppConfig({
|
||||||
|
ui: {
|
||||||
|
select: {
|
||||||
|
slots: {
|
||||||
|
content: 'min-w-fit'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
@@ -709,6 +736,14 @@ collapse: true
|
|||||||
|
|
||||||
:component-emits
|
:component-emits
|
||||||
|
|
||||||
|
### Expose
|
||||||
|
|
||||||
|
When accessing the component via a template ref, you can use the following:
|
||||||
|
|
||||||
|
| Name | Type |
|
||||||
|
| ---- | ---- |
|
||||||
|
| `triggerRef`{lang="ts-type"} | `Ref<InstanceType<typeof SelectTrigger> \| null>`{lang="ts-type"} |
|
||||||
|
|
||||||
## Theme
|
## Theme
|
||||||
|
|
||||||
:component-theme
|
:component-theme
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### Tooltip :badge{label="New" class="align-text-top"}
|
### Tooltip
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ Use the `columns` prop as an array of [ColumnDef](https://tanstack.com/table/lat
|
|||||||
|
|
||||||
- `accessorKey`: [The key of the row object to use when extracting the value for the column.]{class="text-muted"}
|
- `accessorKey`: [The key of the row object to use when extracting the value for the column.]{class="text-muted"}
|
||||||
- `header`: [The header to display for the column. If a string is passed, it can be used as a default for the column ID. If a function is passed, it will be passed a props object for the header and should return the rendered header value (the exact type depends on the adapter being used).]{class="text-muted"}
|
- `header`: [The header to display for the column. If a string is passed, it can be used as a default for the column ID. If a function is passed, it will be passed a props object for the header and should return the rendered header value (the exact type depends on the adapter being used).]{class="text-muted"}
|
||||||
|
- `footer`: [The footer to display for the column. Works exactly like header, but is displayed under the table.]{class="text-muted"}
|
||||||
- `cell`: [The cell to display each row for the column. If a function is passed, it will be passed a props object for the cell and should return the rendered cell value (the exact type depends on the adapter being used).]{class="text-muted"}
|
- `cell`: [The cell to display each row for the column. If a function is passed, it will be passed a props object for the cell and should return the rendered cell value (the exact type depends on the adapter being used).]{class="text-muted"}
|
||||||
- `meta`: [Extra properties for the column.]{class="text-muted"}
|
- `meta`: [Extra properties for the column.]{class="text-muted"}
|
||||||
- `class`:
|
- `class`:
|
||||||
@@ -161,7 +162,7 @@ props:
|
|||||||
|
|
||||||
### Sticky
|
### Sticky
|
||||||
|
|
||||||
Use the `sticky` prop to make the header sticky.
|
Use the `sticky` prop to make the header or footer sticky.
|
||||||
|
|
||||||
::component-code
|
::component-code
|
||||||
---
|
---
|
||||||
@@ -172,6 +173,10 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- data
|
- data
|
||||||
|
items:
|
||||||
|
sticky:
|
||||||
|
- true
|
||||||
|
- false
|
||||||
props:
|
props:
|
||||||
sticky: true
|
sticky: true
|
||||||
data:
|
data:
|
||||||
@@ -266,8 +271,8 @@ You can group rows based on a given column value and show/hide sub rows via some
|
|||||||
|
|
||||||
#### Important parts:
|
#### Important parts:
|
||||||
|
|
||||||
* Add prop `grouping` to `UTable` component with an array of column ids you want to group by.
|
* Add `grouping` prop 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.
|
* Add `grouping-options` prop. 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.
|
* 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.
|
* 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.
|
* `agregatedCell` renderer on column definition only works if there is no `cell` renderer.
|
||||||
@@ -304,19 +309,19 @@ class: '!p-0'
|
|||||||
You can use the `row-selection` prop to control the selection state of the rows (can be binded with `v-model`).
|
You can use the `row-selection` prop to control the selection state of the rows (can be binded with `v-model`).
|
||||||
::
|
::
|
||||||
|
|
||||||
### With `@select` event
|
### With row select event
|
||||||
|
|
||||||
You can add a `@select` listener to make rows clickable. The handler function receives the `TableRow` instance as the first argument and an optional `Event` as the second argument.
|
You can add a `@select` listener to make rows clickable with or without a checkbox column.
|
||||||
|
|
||||||
::note
|
::note
|
||||||
You can use this to navigate to a page, open a modal or even to select the row manually.
|
The handler function receives the `TableRow` instance as the first argument and an optional `Event` as the second argument.
|
||||||
::
|
::
|
||||||
|
|
||||||
::component-example
|
::component-example
|
||||||
---
|
---
|
||||||
prettier: true
|
prettier: true
|
||||||
collapse: true
|
collapse: true
|
||||||
name: 'table-row-selection-event-example'
|
name: 'table-row-select-event-example'
|
||||||
highlights:
|
highlights:
|
||||||
- 123
|
- 123
|
||||||
- 130
|
- 130
|
||||||
@@ -324,6 +329,70 @@ class: '!p-0'
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
::tip
|
||||||
|
You can use this to navigate to a page, open a modal or even to select the row manually.
|
||||||
|
::
|
||||||
|
|
||||||
|
### With row context menu event :badge{label="Soon" class="align-text-top"}
|
||||||
|
|
||||||
|
You can add a `@contextmenu` listener to make rows right clickable and wrap the Table in a [ContextMenu](/components/context-menu) component to display row actions for example.
|
||||||
|
|
||||||
|
::note
|
||||||
|
The handler function receives the `Event` and `TableRow` instance as the first and second arguments respectively.
|
||||||
|
::
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
prettier: true
|
||||||
|
collapse: true
|
||||||
|
name: 'table-row-context-menu-event-example'
|
||||||
|
highlights:
|
||||||
|
- 130
|
||||||
|
- 170
|
||||||
|
class: '!p-0'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### With row hover event :badge{label="Soon" class="align-text-top"}
|
||||||
|
|
||||||
|
You can add a `@hover` listener to make rows hoverable and use a [Popover](/components/popover) or a [Tooltip](/components/tooltip) component to display row details for example.
|
||||||
|
|
||||||
|
::note
|
||||||
|
The handler function receives the `Event` and `TableRow` instance as the first and second arguments respectively.
|
||||||
|
::
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
prettier: true
|
||||||
|
collapse: true
|
||||||
|
name: 'table-row-hover-event-example'
|
||||||
|
highlights:
|
||||||
|
- 126
|
||||||
|
- 149
|
||||||
|
class: '!p-0'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
::note
|
||||||
|
This example is similar as the Popover [with following cursor example](/components/popover#with-following-cursor) and uses a [`refDebounced`](https://vueuse.org/shared/refDebounced/#refdebounced) to prevent the Popover from opening and closing too quickly when moving the cursor from one row to another.
|
||||||
|
::
|
||||||
|
|
||||||
|
### With column footer :badge{label="Soon" class="align-text-top"}
|
||||||
|
|
||||||
|
You can add a `footer` property to the column definition to render a footer for the column.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
prettier: true
|
||||||
|
collapse: true
|
||||||
|
name: 'table-column-footer-example'
|
||||||
|
highlights:
|
||||||
|
- 94
|
||||||
|
- 108
|
||||||
|
class: '!p-0'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
### With column sorting
|
### With column sorting
|
||||||
|
|
||||||
You can update a column `header` to render a [Button](/components/button) component inside the `header` to toggle the sorting state using the TanStack Table [Sorting APIs](https://tanstack.com/table/latest/docs/api/features/sorting).
|
You can update a column `header` to render a [Button](/components/button) component inside the `header` to toggle the sorting state using the TanStack Table [Sorting APIs](https://tanstack.com/table/latest/docs/api/features/sorting).
|
||||||
|
|||||||
@@ -242,6 +242,14 @@ You will have access to the following slots:
|
|||||||
|
|
||||||
:component-emits
|
:component-emits
|
||||||
|
|
||||||
|
### Expose
|
||||||
|
|
||||||
|
When accessing the component via a template ref, you can use the following:
|
||||||
|
|
||||||
|
| Name | Type |
|
||||||
|
| ---- | ---- |
|
||||||
|
| `triggersRef`{lang="ts-type"} | `Ref<ComponentPublicInstance[]>`{lang="ts-type"} |
|
||||||
|
|
||||||
## Theme
|
## Theme
|
||||||
|
|
||||||
:component-theme
|
:component-theme
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### Icon :badge{label="New" class="align-text-top"}
|
### Icon
|
||||||
|
|
||||||
Use the `icon` prop to show an [Icon](/components/icon) inside the Textarea.
|
Use the `icon` prop to show an [Icon](/components/icon) inside the Textarea.
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### Avatar :badge{label="New" class="align-text-top"}
|
### Avatar
|
||||||
|
|
||||||
Use the `avatar` prop to show an [Avatar](/components/avatar) inside the Textarea.
|
Use the `avatar` prop to show an [Avatar](/components/avatar) inside the Textarea.
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### Loading :badge{label="New" class="align-text-top"}
|
### Loading
|
||||||
|
|
||||||
Use the `loading` prop to show a loading icon on the Textarea.
|
Use the `loading` prop to show a loading icon on the Textarea.
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### Loading Icon :badge{label="New" class="align-text-top"}
|
### Loading Icon
|
||||||
|
|
||||||
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
|
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ links:
|
|||||||
- label: GitHub
|
- label: GitHub
|
||||||
icon: i-simple-icons-github
|
icon: i-simple-icons-github
|
||||||
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Timeline.vue
|
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Timeline.vue
|
||||||
navigation.badge: Soon
|
navigation.badge: New
|
||||||
---
|
---
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -173,6 +173,46 @@ class: 'overflow-x-auto'
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Reverse
|
||||||
|
|
||||||
|
Use the reverse prop to reverse the direction of the Timeline.
|
||||||
|
|
||||||
|
::component-code
|
||||||
|
---
|
||||||
|
ignore:
|
||||||
|
- items
|
||||||
|
- class
|
||||||
|
- defaultValue
|
||||||
|
external:
|
||||||
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- TimelineItem[]
|
||||||
|
props:
|
||||||
|
reverse: true
|
||||||
|
modelValue: 2
|
||||||
|
orientation: 'vertical'
|
||||||
|
items:
|
||||||
|
- date: 'Mar 15, 2025'
|
||||||
|
title: 'Project Kickoff'
|
||||||
|
description: 'Kicked off the project with team alignment.'
|
||||||
|
icon: 'i-lucide-rocket'
|
||||||
|
- date: 'Mar 22 2025'
|
||||||
|
title: 'Design Phase'
|
||||||
|
description: 'User research and design workshops.'
|
||||||
|
icon: 'i-lucide-palette'
|
||||||
|
- date: 'Mar 29 2025'
|
||||||
|
title: 'Development Sprint'
|
||||||
|
description: 'Frontend and backend development.'
|
||||||
|
icon: 'i-lucide-code'
|
||||||
|
- date: 'Apr 5 2025'
|
||||||
|
title: 'Testing & Deployment'
|
||||||
|
description: 'QA testing and performance optimization.'
|
||||||
|
icon: 'i-lucide-check-circle'
|
||||||
|
class: 'w-full'
|
||||||
|
class: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Control active item
|
### Control active item
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ name: 'toast-color-example'
|
|||||||
|
|
||||||
### Close
|
### Close
|
||||||
|
|
||||||
Pass a `close` field to customize or hide the close button (with `false` value).
|
Pass a `close` field to customize or hide the close [Button](/components/button) (with `false` value).
|
||||||
|
|
||||||
::component-example
|
::component-example
|
||||||
---
|
---
|
||||||
@@ -143,7 +143,7 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.cl
|
|||||||
|
|
||||||
### Actions
|
### Actions
|
||||||
|
|
||||||
Pass an `actions` field to add some [Button](/components/button) actions to the Alert.
|
Pass an `actions` field to add some [Button](/components/button) actions to the Toast.
|
||||||
|
|
||||||
::component-example
|
::component-example
|
||||||
---
|
---
|
||||||
@@ -155,9 +155,23 @@ name: 'toast-actions-example'
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Progress :badge{label="Soon" class="align-text-top"}
|
||||||
|
|
||||||
|
Pass a `progress` field to customize or hide the [Progress](/components/progress) bar (with `false` value).
|
||||||
|
|
||||||
|
::tip
|
||||||
|
The Progress bar inherits the Toast color by default, but you can override it using the `progress.color` field.
|
||||||
|
::
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
name: 'toast-progress-example'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
### Orientation
|
### Orientation
|
||||||
|
|
||||||
Use the `orientation` prop to change the orientation of the Toast.
|
Pass an `orientation` field to the `toast.add` method to change the orientation of the Toast.
|
||||||
|
|
||||||
::component-example
|
::component-example
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -186,6 +186,16 @@ name: 'tooltip-open-example'
|
|||||||
In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts), you can toggle the Tooltip by pressing :kbd{value="O"}.
|
In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts), you can toggle the Tooltip by pressing :kbd{value="O"}.
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### With following cursor :badge{label="Soon" class="align-text-top"}
|
||||||
|
|
||||||
|
You can make the Tooltip follow the cursor when hovering over an element using the [`reference`](https://reka-ui.com/docs/components/tooltip#trigger) prop:
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
name: 'tooltip-cursor-example'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
|
|||||||
@@ -11,39 +11,40 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/vue": "^1.2.12",
|
"@ai-sdk/vue": "^1.2.12",
|
||||||
"@iconify-json/logos": "^1.2.4",
|
"@iconify-json/logos": "^1.2.4",
|
||||||
"@iconify-json/lucide": "^1.2.47",
|
"@iconify-json/lucide": "^1.2.56",
|
||||||
"@iconify-json/simple-icons": "^1.2.38",
|
"@iconify-json/simple-icons": "^1.2.42",
|
||||||
"@iconify-json/vscode-icons": "^1.2.22",
|
"@iconify-json/vscode-icons": "^1.2.23",
|
||||||
"@nuxt/content": "^3.5.1",
|
"@nuxt/content": "^3.6.3",
|
||||||
"@nuxt/image": "^1.10.0",
|
"@nuxt/image": "^1.10.0",
|
||||||
"@nuxt/ui": "workspace:*",
|
"@nuxt/ui": "workspace:*",
|
||||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@beebbd4",
|
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@17684e4",
|
||||||
"@nuxthub/core": "^0.9.0",
|
"@nuxthub/core": "^0.9.0",
|
||||||
"@nuxtjs/plausible": "^1.2.0",
|
"@nuxtjs/plausible": "^1.2.0",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@rollup/plugin-yaml": "^4.1.2",
|
"@rollup/plugin-yaml": "^4.1.2",
|
||||||
"@vueuse/integrations": "^13.3.0",
|
"@vueuse/integrations": "^13.5.0",
|
||||||
"@vueuse/nuxt": "^13.3.0",
|
"@vueuse/nuxt": "^13.5.0",
|
||||||
"ai": "^4.3.16",
|
"ai": "^4.3.16",
|
||||||
|
"better-sqlite3": "^12.2.0",
|
||||||
"capture-website": "^4.2.0",
|
"capture-website": "^4.2.0",
|
||||||
"joi": "^17.13.3",
|
"joi": "^17.13.3",
|
||||||
"maska": "^3.1.1",
|
"maska": "^3.2.0",
|
||||||
"motion-v": "^1.2.1",
|
"motion-v": "^1.5.0",
|
||||||
"nuxt": "^3.17.5",
|
"nuxt": "4.0.0-rc.0",
|
||||||
"nuxt-component-meta": "^0.11.0",
|
"nuxt-component-meta": "^0.12.1",
|
||||||
"nuxt-llms": "^0.1.3",
|
"nuxt-llms": "^0.1.3",
|
||||||
"nuxt-og-image": "^5.1.6",
|
"nuxt-og-image": "^5.1.9",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.2",
|
||||||
"shiki-transformer-color-highlight": "^1.0.0",
|
"shiki-transformer-color-highlight": "^1.0.0",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"superstruct": "^2.0.2",
|
"superstruct": "^2.0.2",
|
||||||
"ufo": "^1.6.1",
|
"ufo": "^1.6.1",
|
||||||
"valibot": "^1.1.0",
|
"valibot": "^1.1.0",
|
||||||
"workers-ai-provider": "^0.6.0",
|
"workers-ai-provider": "^0.7.1",
|
||||||
"yup": "^1.6.1",
|
"yup": "^1.6.1",
|
||||||
"zod": "^3.25.57"
|
"zod": "^3.25.75"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"wrangler": "^4.19.1"
|
"wrangler": "^4.23.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
docs/public/components/dark/changelog-version.png
Normal file
BIN
docs/public/components/dark/changelog-version.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
BIN
docs/public/components/dark/changelog-versions.png
Normal file
BIN
docs/public/components/dark/changelog-versions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/public/components/dark/input-tags.png
Normal file
BIN
docs/public/components/dark/input-tags.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
docs/public/components/light/changelog-version.png
Normal file
BIN
docs/public/components/light/changelog-version.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
docs/public/components/light/changelog-versions.png
Normal file
BIN
docs/public/components/light/changelog-versions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
BIN
docs/public/components/light/input-tags.png
Normal file
BIN
docs/public/components/light/input-tags.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -1,412 +1,8 @@
|
|||||||
import json5 from 'json5'
|
|
||||||
import { camelCase, kebabCase } from 'scule'
|
|
||||||
import { visit } from '@nuxt/content/runtime'
|
|
||||||
import type { H3Event } from 'h3'
|
import type { H3Event } from 'h3'
|
||||||
import type { PageCollectionItemBase } from '@nuxt/content'
|
import type { PageCollectionItemBase } from '@nuxt/content'
|
||||||
import * as theme from '../../.nuxt/ui'
|
|
||||||
import * as themePro from '../../.nuxt/ui-pro'
|
|
||||||
import meta from '#nuxt-component-meta'
|
|
||||||
// @ts-expect-error - no types available
|
|
||||||
import components from '#component-example/nitro'
|
|
||||||
|
|
||||||
type ComponentAttributes = {
|
|
||||||
':pro'?: string
|
|
||||||
':prose'?: string
|
|
||||||
':props'?: string
|
|
||||||
':external'?: string
|
|
||||||
':externalTypes'?: string
|
|
||||||
':ignore'?: string
|
|
||||||
':hide'?: string
|
|
||||||
':slots'?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ThemeConfig = {
|
|
||||||
pro: boolean
|
|
||||||
prose: boolean
|
|
||||||
componentName: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CodeConfig = {
|
|
||||||
pro: boolean
|
|
||||||
props: Record<string, unknown>
|
|
||||||
external: string[]
|
|
||||||
externalTypes: string[]
|
|
||||||
ignore: string[]
|
|
||||||
hide: string[]
|
|
||||||
componentName: string
|
|
||||||
slots?: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
type Document = {
|
|
||||||
title: string
|
|
||||||
body: any
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseBoolean = (value?: string): boolean => value === 'true'
|
|
||||||
|
|
||||||
function getComponentMeta(componentName: string) {
|
|
||||||
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
|
|
||||||
|
|
||||||
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: finalMetaComponentName,
|
|
||||||
componentMeta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceNodeWithPre(node: any[], language: string, code: string, filename?: string) {
|
|
||||||
node[0] = 'pre'
|
|
||||||
node[1] = { language, code }
|
|
||||||
if (filename) node[1].filename = filename
|
|
||||||
}
|
|
||||||
|
|
||||||
function visitAndReplace(doc: Document, type: string, handler: (node: any[]) => void) {
|
|
||||||
visit(doc.body, (node) => {
|
|
||||||
if (Array.isArray(node) && node[0] === type) {
|
|
||||||
handler(node)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}, node => node)
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateTSInterface(
|
|
||||||
name: string,
|
|
||||||
items: any[],
|
|
||||||
itemHandler: (item: any) => string,
|
|
||||||
description: string
|
|
||||||
) {
|
|
||||||
let code = `/**\n * ${description}\n */\ninterface ${name} {\n`
|
|
||||||
for (const item of items) {
|
|
||||||
code += itemHandler(item)
|
|
||||||
}
|
|
||||||
code += `}`
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
function propItemHandler(propValue: any): string {
|
|
||||||
if (!propValue?.name) return ''
|
|
||||||
const propName = propValue.name
|
|
||||||
const propType = propValue.type
|
|
||||||
? Array.isArray(propValue.type)
|
|
||||||
? propValue.type.map((t: any) => t.name || t).join(' | ')
|
|
||||||
: propValue.type.name || propValue.type
|
|
||||||
: 'any'
|
|
||||||
const isRequired = propValue.required || false
|
|
||||||
const hasDescription = propValue.description && propValue.description.trim().length > 0
|
|
||||||
const hasDefault = propValue.default !== undefined
|
|
||||||
let result = ''
|
|
||||||
if (hasDescription || hasDefault) {
|
|
||||||
result += ` /**\n`
|
|
||||||
if (hasDescription) {
|
|
||||||
const descLines = propValue.description.split(/\r?\n/)
|
|
||||||
descLines.forEach((line: string) => {
|
|
||||||
result += ` * ${line}\n`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (hasDefault) {
|
|
||||||
let defaultValue = propValue.default
|
|
||||||
if (typeof defaultValue === 'string') {
|
|
||||||
defaultValue = `"${defaultValue.replace(/"/g, '\\"')}"`
|
|
||||||
} else {
|
|
||||||
defaultValue = JSON.stringify(defaultValue)
|
|
||||||
}
|
|
||||||
result += ` * @default ${defaultValue}\n`
|
|
||||||
}
|
|
||||||
result += ` */\n`
|
|
||||||
}
|
|
||||||
result += ` ${propName}${isRequired ? '' : '?'}: ${propType};\n`
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function slotItemHandler(slotValue: any): string {
|
|
||||||
if (!slotValue?.name) return ''
|
|
||||||
const slotName = slotValue.name
|
|
||||||
const hasDescription = slotValue.description && slotValue.description.trim().length > 0
|
|
||||||
let result = ''
|
|
||||||
if (hasDescription) {
|
|
||||||
result += ` /**\n`
|
|
||||||
const descLines = slotValue.description.split(/\r?\n/)
|
|
||||||
descLines.forEach((line: string) => {
|
|
||||||
result += ` * ${line}\n`
|
|
||||||
})
|
|
||||||
result += ` */\n`
|
|
||||||
}
|
|
||||||
if (slotValue.bindings && Object.keys(slotValue.bindings).length > 0) {
|
|
||||||
let bindingsType = '{\n'
|
|
||||||
Object.entries(slotValue.bindings).forEach(([bindingName, bindingValue]: [string, any]) => {
|
|
||||||
const bindingType = bindingValue.type || 'any'
|
|
||||||
bindingsType += ` ${bindingName}: ${bindingType};\n`
|
|
||||||
})
|
|
||||||
bindingsType += ' }'
|
|
||||||
result += ` ${slotName}(bindings: ${bindingsType}): any;\n`
|
|
||||||
} else {
|
|
||||||
result += ` ${slotName}(): any;\n`
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitItemHandler(event: any): string {
|
|
||||||
if (!event?.name) return ''
|
|
||||||
let payloadType = 'void'
|
|
||||||
if (event.type) {
|
|
||||||
payloadType = Array.isArray(event.type)
|
|
||||||
? event.type.map((t: any) => t.name || t).join(' | ')
|
|
||||||
: event.type.name || event.type
|
|
||||||
}
|
|
||||||
let result = ''
|
|
||||||
if (event.description && event.description.trim().length > 0) {
|
|
||||||
result += ` /**\n`
|
|
||||||
event.description.split(/\r?\n/).forEach((line: string) => {
|
|
||||||
result += ` * ${line}\n`
|
|
||||||
})
|
|
||||||
result += ` */\n`
|
|
||||||
}
|
|
||||||
result += ` ${event.name}: (payload: ${payloadType}) => void;\n`
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
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 } }
|
|
||||||
: { [componentName]: componentTheme }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateComponentCode = ({
|
|
||||||
pro,
|
|
||||||
props,
|
|
||||||
external,
|
|
||||||
externalTypes,
|
|
||||||
hide,
|
|
||||||
componentName,
|
|
||||||
slots
|
|
||||||
}: CodeConfig) => {
|
|
||||||
const filteredProps = Object.fromEntries(
|
|
||||||
Object.entries(props).filter(([key]) => !hide.includes(key))
|
|
||||||
)
|
|
||||||
|
|
||||||
const imports = pro
|
|
||||||
? ''
|
|
||||||
: external
|
|
||||||
.filter((_, index) => externalTypes[index] && externalTypes[index] !== 'undefined')
|
|
||||||
.map((ext, index) => {
|
|
||||||
const type = externalTypes[index]?.replace(/[[\]]/g, '')
|
|
||||||
return `import type { ${type} } from '@nuxt/${pro ? 'ui-pro' : 'ui'}'`
|
|
||||||
})
|
|
||||||
.join('\n')
|
|
||||||
|
|
||||||
let itemsCode = ''
|
|
||||||
if (props.items) {
|
|
||||||
itemsCode = pro
|
|
||||||
? `const items = ref(${json5.stringify(props.items, null, 2)})`
|
|
||||||
: `const items = ref<${externalTypes[0]}>(${json5.stringify(props.items, null, 2)})`
|
|
||||||
delete filteredProps.items
|
|
||||||
}
|
|
||||||
|
|
||||||
let calendarValueCode = ''
|
|
||||||
if (componentName === 'calendar' && props.modelValue && Array.isArray(props.modelValue)) {
|
|
||||||
calendarValueCode = `const value = ref(new CalendarDate(${props.modelValue.join(', ')}))`
|
|
||||||
}
|
|
||||||
|
|
||||||
const propsString = Object.entries(filteredProps)
|
|
||||||
.map(([key, value]) => {
|
|
||||||
const formattedKey = kebabCase(key)
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return `${formattedKey}="${value}"`
|
|
||||||
} else if (typeof value === 'number') {
|
|
||||||
return `:${formattedKey}="${value}"`
|
|
||||||
} else if (typeof value === 'boolean') {
|
|
||||||
return value ? formattedKey : `:${formattedKey}="false"`
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')
|
|
||||||
|
|
||||||
const itemsProp = props.items ? ':items="items"' : ''
|
|
||||||
const vModelProp = componentName === 'calendar' && props.modelValue ? 'v-model="value"' : ''
|
|
||||||
const allProps = [propsString, itemsProp, vModelProp].filter(Boolean).join(' ')
|
|
||||||
const formattedProps = allProps ? ` ${allProps}` : ''
|
|
||||||
|
|
||||||
let scriptSetup = ''
|
|
||||||
if (imports || itemsCode || calendarValueCode) {
|
|
||||||
scriptSetup = '<script setup lang="ts">'
|
|
||||||
if (imports) scriptSetup += `\n${imports}`
|
|
||||||
if (imports && (itemsCode || calendarValueCode)) scriptSetup += '\n'
|
|
||||||
if (calendarValueCode) scriptSetup += `\n${calendarValueCode}`
|
|
||||||
if (itemsCode) scriptSetup += `\n${itemsCode}`
|
|
||||||
scriptSetup += '\n</script>\n\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
let componentContent = ''
|
|
||||||
let slotContent = ''
|
|
||||||
|
|
||||||
if (slots && Object.keys(slots).length > 0) {
|
|
||||||
const defaultSlot = slots.default?.trim()
|
|
||||||
if (defaultSlot) {
|
|
||||||
const indentedContent = defaultSlot
|
|
||||||
.split('\n')
|
|
||||||
.map(line => line.trim() ? ` ${line}` : line)
|
|
||||||
.join('\n')
|
|
||||||
componentContent = `\n${indentedContent}\n `
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(slots).forEach(([slotName, content]) => {
|
|
||||||
if (slotName !== 'default' && content?.trim()) {
|
|
||||||
const indentedSlotContent = content.trim()
|
|
||||||
.split('\n')
|
|
||||||
.map(line => line.trim() ? ` ${line}` : line)
|
|
||||||
.join('\n')
|
|
||||||
slotContent += `\n <template #${slotName}>\n${indentedSlotContent}\n </template>`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
|
|
||||||
|
|
||||||
let componentTemplate = ''
|
|
||||||
if (componentContent || slotContent) {
|
|
||||||
componentTemplate = `<U${pascalCaseName}${formattedProps}>${componentContent}${slotContent}</U${pascalCaseName}>` // Removed space before closing tag
|
|
||||||
} else {
|
|
||||||
componentTemplate = `<U${pascalCaseName}${formattedProps} />`
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${scriptSetup}<template>
|
|
||||||
${componentTemplate}
|
|
||||||
</template>`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineNitroPlugin((nitroApp) => {
|
export default defineNitroPlugin((nitroApp) => {
|
||||||
nitroApp.hooks.hook('content:llms:generate:document', async (_: H3Event, doc: PageCollectionItemBase) => {
|
nitroApp.hooks.hook('content:llms:generate:document', async (_: H3Event, doc: PageCollectionItemBase) => {
|
||||||
const componentName = camelCase(doc.title)
|
transformMDC(doc as any)
|
||||||
|
|
||||||
visitAndReplace(doc, 'component-theme', (node) => {
|
|
||||||
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: finalComponentName })
|
|
||||||
|
|
||||||
replaceNodeWithPre(
|
|
||||||
node,
|
|
||||||
'ts',
|
|
||||||
`export default defineAppConfig(${json5.stringify(appConfig, null, 2)?.replace(/,([ |\t\n]+[}|\])])/g, '$1')})`,
|
|
||||||
'app.config.ts'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
visitAndReplace(doc, 'component-code', (node) => {
|
|
||||||
const attributes = node[1] as ComponentAttributes
|
|
||||||
const pro = parseBoolean(attributes[':pro'])
|
|
||||||
const props = attributes[':props'] ? json5.parse(attributes[':props']) : {}
|
|
||||||
const external = attributes[':external'] ? json5.parse(attributes[':external']) : []
|
|
||||||
const externalTypes = attributes[':externalTypes'] ? json5.parse(attributes[':externalTypes']) : []
|
|
||||||
const ignore = attributes[':ignore'] ? json5.parse(attributes[':ignore']) : []
|
|
||||||
const hide = attributes[':hide'] ? json5.parse(attributes[':hide']) : []
|
|
||||||
const slots = attributes[':slots'] ? json5.parse(attributes[':slots']) : {}
|
|
||||||
|
|
||||||
const code = generateComponentCode({
|
|
||||||
pro,
|
|
||||||
props,
|
|
||||||
external,
|
|
||||||
externalTypes,
|
|
||||||
ignore,
|
|
||||||
hide,
|
|
||||||
componentName,
|
|
||||||
slots
|
|
||||||
})
|
|
||||||
|
|
||||||
replaceNodeWithPre(node, 'vue', code)
|
|
||||||
})
|
|
||||||
|
|
||||||
visitAndReplace(doc, 'component-props', (node) => {
|
|
||||||
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(
|
|
||||||
interfaceName,
|
|
||||||
Object.values(componentMeta.props),
|
|
||||||
propItemHandler,
|
|
||||||
`Props for the ${isProse ? 'Prose' : ''}${pascalCaseName} component`
|
|
||||||
)
|
|
||||||
replaceNodeWithPre(node, 'ts', interfaceCode)
|
|
||||||
})
|
|
||||||
|
|
||||||
visitAndReplace(doc, 'component-slots', (node) => {
|
|
||||||
const { pascalCaseName, componentMeta } = getComponentMeta(componentName)
|
|
||||||
if (!componentMeta?.slots) return
|
|
||||||
|
|
||||||
const interfaceCode = generateTSInterface(
|
|
||||||
`${pascalCaseName}Slots`,
|
|
||||||
Object.values(componentMeta.slots),
|
|
||||||
slotItemHandler,
|
|
||||||
`Slots for the ${pascalCaseName} component`
|
|
||||||
)
|
|
||||||
replaceNodeWithPre(node, 'ts', interfaceCode)
|
|
||||||
})
|
|
||||||
|
|
||||||
visitAndReplace(doc, 'component-emits', (node) => {
|
|
||||||
const { pascalCaseName, componentMeta } = getComponentMeta(componentName)
|
|
||||||
const hasEvents = componentMeta?.events && Object.keys(componentMeta.events).length > 0
|
|
||||||
|
|
||||||
if (hasEvents) {
|
|
||||||
const interfaceCode = generateTSInterface(
|
|
||||||
`${pascalCaseName}Emits`,
|
|
||||||
Object.values(componentMeta.events),
|
|
||||||
emitItemHandler,
|
|
||||||
`Emitted events for the ${pascalCaseName} component`
|
|
||||||
)
|
|
||||||
replaceNodeWithPre(node, 'ts', interfaceCode)
|
|
||||||
} else {
|
|
||||||
node[0] = 'p'
|
|
||||||
node[1] = {}
|
|
||||||
node[2] = 'No events available for this component.'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
visitAndReplace(doc, 'component-example', (node) => {
|
|
||||||
const camelName = camelCase(node[1]['name'])
|
|
||||||
const name = camelName.charAt(0).toUpperCase() + camelName.slice(1)
|
|
||||||
const code = components[name].code
|
|
||||||
replaceNodeWithPre(node, 'vue', code, `${name}.vue`)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
30
docs/server/routes/raw/[...slug].md.get.ts
Normal file
30
docs/server/routes/raw/[...slug].md.get.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { stringify } from 'minimark/stringify'
|
||||||
|
import { withLeadingSlash } from 'ufo'
|
||||||
|
|
||||||
|
export default eventHandler(async (event) => {
|
||||||
|
const slug = getRouterParams(event)['slug.md']
|
||||||
|
if (!slug?.endsWith('.md')) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = withLeadingSlash(slug.replace('.md', ''))
|
||||||
|
// @ts-expect-error TODO: fix this
|
||||||
|
const page = await queryCollection(event, 'content').path(path).first()
|
||||||
|
if (!page) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add title and description to the top of the page if missing
|
||||||
|
if (page.body.value[0]?.[0] !== 'h1') {
|
||||||
|
page.body.value.unshift(['blockquote', {}, page.description])
|
||||||
|
page.body.value.unshift(['h1', {}, page.title])
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformedPage = transformMDC({
|
||||||
|
title: page.title,
|
||||||
|
body: page.body
|
||||||
|
})
|
||||||
|
|
||||||
|
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
|
||||||
|
return stringify({ ...transformedPage.body, type: 'minimark' }, { format: 'markdown/html' })
|
||||||
|
})
|
||||||
410
docs/server/utils/transformMDC.ts
Normal file
410
docs/server/utils/transformMDC.ts
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
import json5 from 'json5'
|
||||||
|
import { camelCase, kebabCase } from 'scule'
|
||||||
|
import { visit } from '@nuxt/content/runtime'
|
||||||
|
import * as theme from '../../.nuxt/ui'
|
||||||
|
import * as themePro from '../../.nuxt/ui-pro'
|
||||||
|
import meta from '#nuxt-component-meta'
|
||||||
|
// @ts-expect-error - no types available
|
||||||
|
import components from '#component-example/nitro'
|
||||||
|
|
||||||
|
type ComponentAttributes = {
|
||||||
|
':pro'?: string
|
||||||
|
':prose'?: string
|
||||||
|
':props'?: string
|
||||||
|
':external'?: string
|
||||||
|
':externalTypes'?: string
|
||||||
|
':ignore'?: string
|
||||||
|
':hide'?: string
|
||||||
|
':slots'?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeConfig = {
|
||||||
|
pro: boolean
|
||||||
|
prose: boolean
|
||||||
|
componentName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CodeConfig = {
|
||||||
|
pro: boolean
|
||||||
|
props: Record<string, unknown>
|
||||||
|
external: string[]
|
||||||
|
externalTypes: string[]
|
||||||
|
ignore: string[]
|
||||||
|
hide: string[]
|
||||||
|
componentName: string
|
||||||
|
slots?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Document = {
|
||||||
|
title: string
|
||||||
|
body: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseBoolean = (value?: string): boolean => value === 'true'
|
||||||
|
|
||||||
|
function getComponentMeta(componentName: string) {
|
||||||
|
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
|
||||||
|
|
||||||
|
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: finalMetaComponentName,
|
||||||
|
componentMeta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceNodeWithPre(node: any[], language: string, code: string, filename?: string) {
|
||||||
|
node[0] = 'pre'
|
||||||
|
node[1] = { language, code }
|
||||||
|
if (filename) node[1].filename = filename
|
||||||
|
}
|
||||||
|
|
||||||
|
function visitAndReplace(doc: Document, type: string, handler: (node: any[]) => void) {
|
||||||
|
visit(doc.body, (node) => {
|
||||||
|
if (Array.isArray(node) && node[0] === type) {
|
||||||
|
handler(node)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, node => node)
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTSInterface(
|
||||||
|
name: string,
|
||||||
|
items: any[],
|
||||||
|
itemHandler: (item: any) => string,
|
||||||
|
description: string
|
||||||
|
) {
|
||||||
|
let code = `/**\n * ${description}\n */\ninterface ${name} {\n`
|
||||||
|
for (const item of items) {
|
||||||
|
code += itemHandler(item)
|
||||||
|
}
|
||||||
|
code += `}`
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
function propItemHandler(propValue: any): string {
|
||||||
|
if (!propValue?.name) return ''
|
||||||
|
const propName = propValue.name
|
||||||
|
const propType = propValue.type
|
||||||
|
? Array.isArray(propValue.type)
|
||||||
|
? propValue.type.map((t: any) => t.name || t).join(' | ')
|
||||||
|
: propValue.type.name || propValue.type
|
||||||
|
: 'any'
|
||||||
|
const isRequired = propValue.required || false
|
||||||
|
const hasDescription = propValue.description && propValue.description.trim().length > 0
|
||||||
|
const hasDefault = propValue.default !== undefined
|
||||||
|
let result = ''
|
||||||
|
if (hasDescription || hasDefault) {
|
||||||
|
result += ` /**\n`
|
||||||
|
if (hasDescription) {
|
||||||
|
const descLines = propValue.description.split(/\r?\n/)
|
||||||
|
descLines.forEach((line: string) => {
|
||||||
|
result += ` * ${line}\n`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (hasDefault) {
|
||||||
|
let defaultValue = propValue.default
|
||||||
|
if (typeof defaultValue === 'string') {
|
||||||
|
defaultValue = `"${defaultValue.replace(/"/g, '\\"')}"`
|
||||||
|
} else {
|
||||||
|
defaultValue = JSON.stringify(defaultValue)
|
||||||
|
}
|
||||||
|
result += ` * @default ${defaultValue}\n`
|
||||||
|
}
|
||||||
|
result += ` */\n`
|
||||||
|
}
|
||||||
|
result += ` ${propName}${isRequired ? '' : '?'}: ${propType};\n`
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function slotItemHandler(slotValue: any): string {
|
||||||
|
if (!slotValue?.name) return ''
|
||||||
|
const slotName = slotValue.name
|
||||||
|
const hasDescription = slotValue.description && slotValue.description.trim().length > 0
|
||||||
|
let result = ''
|
||||||
|
if (hasDescription) {
|
||||||
|
result += ` /**\n`
|
||||||
|
const descLines = slotValue.description.split(/\r?\n/)
|
||||||
|
descLines.forEach((line: string) => {
|
||||||
|
result += ` * ${line}\n`
|
||||||
|
})
|
||||||
|
result += ` */\n`
|
||||||
|
}
|
||||||
|
if (slotValue.bindings && Object.keys(slotValue.bindings).length > 0) {
|
||||||
|
let bindingsType = '{\n'
|
||||||
|
Object.entries(slotValue.bindings).forEach(([bindingName, bindingValue]: [string, any]) => {
|
||||||
|
const bindingType = bindingValue.type || 'any'
|
||||||
|
bindingsType += ` ${bindingName}: ${bindingType};\n`
|
||||||
|
})
|
||||||
|
bindingsType += ' }'
|
||||||
|
result += ` ${slotName}(bindings: ${bindingsType}): any;\n`
|
||||||
|
} else {
|
||||||
|
result += ` ${slotName}(): any;\n`
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitItemHandler(event: any): string {
|
||||||
|
if (!event?.name) return ''
|
||||||
|
let payloadType = 'void'
|
||||||
|
if (event.type) {
|
||||||
|
payloadType = Array.isArray(event.type)
|
||||||
|
? event.type.map((t: any) => t.name || t).join(' | ')
|
||||||
|
: event.type.name || event.type
|
||||||
|
}
|
||||||
|
let result = ''
|
||||||
|
if (event.description && event.description.trim().length > 0) {
|
||||||
|
result += ` /**\n`
|
||||||
|
event.description.split(/\r?\n/).forEach((line: string) => {
|
||||||
|
result += ` * ${line}\n`
|
||||||
|
})
|
||||||
|
result += ` */\n`
|
||||||
|
}
|
||||||
|
result += ` ${event.name}: (payload: ${payloadType}) => void;\n`
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
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 } }
|
||||||
|
: { [componentName]: componentTheme }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateComponentCode = ({
|
||||||
|
pro,
|
||||||
|
props,
|
||||||
|
external,
|
||||||
|
externalTypes,
|
||||||
|
hide,
|
||||||
|
componentName,
|
||||||
|
slots
|
||||||
|
}: CodeConfig) => {
|
||||||
|
const filteredProps = Object.fromEntries(
|
||||||
|
Object.entries(props).filter(([key]) => !hide.includes(key))
|
||||||
|
)
|
||||||
|
|
||||||
|
const imports = pro
|
||||||
|
? ''
|
||||||
|
: external
|
||||||
|
.filter((_, index) => externalTypes[index] && externalTypes[index] !== 'undefined')
|
||||||
|
.map((ext, index) => {
|
||||||
|
const type = externalTypes[index]?.replace(/[[\]]/g, '')
|
||||||
|
return `import type { ${type} } from '@nuxt/${pro ? 'ui-pro' : 'ui'}'`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
let itemsCode = ''
|
||||||
|
if (props.items) {
|
||||||
|
itemsCode = pro
|
||||||
|
? `const items = ref(${json5.stringify(props.items, null, 2)})`
|
||||||
|
: `const items = ref<${externalTypes[0]}>(${json5.stringify(props.items, null, 2)})`
|
||||||
|
delete filteredProps.items
|
||||||
|
}
|
||||||
|
|
||||||
|
let calendarValueCode = ''
|
||||||
|
if (componentName === 'calendar' && props.modelValue && Array.isArray(props.modelValue)) {
|
||||||
|
calendarValueCode = `const value = ref(new CalendarDate(${props.modelValue.join(', ')}))`
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsString = Object.entries(filteredProps)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const formattedKey = kebabCase(key)
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return `${formattedKey}="${value}"`
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
return `:${formattedKey}="${value}"`
|
||||||
|
} else if (typeof value === 'boolean') {
|
||||||
|
return value ? formattedKey : `:${formattedKey}="false"`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
|
const itemsProp = props.items ? ':items="items"' : ''
|
||||||
|
const vModelProp = componentName === 'calendar' && props.modelValue ? 'v-model="value"' : ''
|
||||||
|
const allProps = [propsString, itemsProp, vModelProp].filter(Boolean).join(' ')
|
||||||
|
const formattedProps = allProps ? ` ${allProps}` : ''
|
||||||
|
|
||||||
|
let scriptSetup = ''
|
||||||
|
if (imports || itemsCode || calendarValueCode) {
|
||||||
|
scriptSetup = '<script setup lang="ts">'
|
||||||
|
if (imports) scriptSetup += `\n${imports}`
|
||||||
|
if (imports && (itemsCode || calendarValueCode)) scriptSetup += '\n'
|
||||||
|
if (calendarValueCode) scriptSetup += `\n${calendarValueCode}`
|
||||||
|
if (itemsCode) scriptSetup += `\n${itemsCode}`
|
||||||
|
scriptSetup += '\n</script>\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
let componentContent = ''
|
||||||
|
let slotContent = ''
|
||||||
|
|
||||||
|
if (slots && Object.keys(slots).length > 0) {
|
||||||
|
const defaultSlot = slots.default?.trim()
|
||||||
|
if (defaultSlot) {
|
||||||
|
const indentedContent = defaultSlot
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim() ? ` ${line}` : line)
|
||||||
|
.join('\n')
|
||||||
|
componentContent = `\n${indentedContent}\n `
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(slots).forEach(([slotName, content]) => {
|
||||||
|
if (slotName !== 'default' && content?.trim()) {
|
||||||
|
const indentedSlotContent = content.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim() ? ` ${line}` : line)
|
||||||
|
.join('\n')
|
||||||
|
slotContent += `\n <template #${slotName}>\n${indentedSlotContent}\n </template>`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
|
||||||
|
|
||||||
|
let componentTemplate = ''
|
||||||
|
if (componentContent || slotContent) {
|
||||||
|
componentTemplate = `<U${pascalCaseName}${formattedProps}>${componentContent}${slotContent}</U${pascalCaseName}>` // Removed space before closing tag
|
||||||
|
} else {
|
||||||
|
componentTemplate = `<U${pascalCaseName}${formattedProps} />`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${scriptSetup}<template>
|
||||||
|
${componentTemplate}
|
||||||
|
</template>`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformMDC(doc: Document): Document {
|
||||||
|
const componentName = camelCase(doc.title)
|
||||||
|
|
||||||
|
visitAndReplace(doc, 'component-theme', (node) => {
|
||||||
|
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: finalComponentName })
|
||||||
|
|
||||||
|
replaceNodeWithPre(
|
||||||
|
node,
|
||||||
|
'ts',
|
||||||
|
`export default defineAppConfig(${json5.stringify(appConfig, null, 2)?.replace(/,([ |\t\n]+[}|\])])/g, '$1')})`,
|
||||||
|
'app.config.ts'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
visitAndReplace(doc, 'component-code', (node) => {
|
||||||
|
const attributes = node[1] as ComponentAttributes
|
||||||
|
const pro = parseBoolean(attributes[':pro'])
|
||||||
|
const props = attributes[':props'] ? json5.parse(attributes[':props']) : {}
|
||||||
|
const external = attributes[':external'] ? json5.parse(attributes[':external']) : []
|
||||||
|
const externalTypes = attributes[':externalTypes'] ? json5.parse(attributes[':externalTypes']) : []
|
||||||
|
const ignore = attributes[':ignore'] ? json5.parse(attributes[':ignore']) : []
|
||||||
|
const hide = attributes[':hide'] ? json5.parse(attributes[':hide']) : []
|
||||||
|
const slots = attributes[':slots'] ? json5.parse(attributes[':slots']) : {}
|
||||||
|
|
||||||
|
const code = generateComponentCode({
|
||||||
|
pro,
|
||||||
|
props,
|
||||||
|
external,
|
||||||
|
externalTypes,
|
||||||
|
ignore,
|
||||||
|
hide,
|
||||||
|
componentName,
|
||||||
|
slots
|
||||||
|
})
|
||||||
|
|
||||||
|
replaceNodeWithPre(node, 'vue', code)
|
||||||
|
})
|
||||||
|
|
||||||
|
visitAndReplace(doc, 'component-props', (node) => {
|
||||||
|
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(
|
||||||
|
interfaceName,
|
||||||
|
Object.values(componentMeta.props),
|
||||||
|
propItemHandler,
|
||||||
|
`Props for the ${isProse ? 'Prose' : ''}${pascalCaseName} component`
|
||||||
|
)
|
||||||
|
replaceNodeWithPre(node, 'ts', interfaceCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
visitAndReplace(doc, 'component-slots', (node) => {
|
||||||
|
const { pascalCaseName, componentMeta } = getComponentMeta(componentName)
|
||||||
|
if (!componentMeta?.slots) return
|
||||||
|
|
||||||
|
const interfaceCode = generateTSInterface(
|
||||||
|
`${pascalCaseName}Slots`,
|
||||||
|
Object.values(componentMeta.slots),
|
||||||
|
slotItemHandler,
|
||||||
|
`Slots for the ${pascalCaseName} component`
|
||||||
|
)
|
||||||
|
replaceNodeWithPre(node, 'ts', interfaceCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
visitAndReplace(doc, 'component-emits', (node) => {
|
||||||
|
const { pascalCaseName, componentMeta } = getComponentMeta(componentName)
|
||||||
|
const hasEvents = componentMeta?.events && Object.keys(componentMeta.events).length > 0
|
||||||
|
|
||||||
|
if (hasEvents) {
|
||||||
|
const interfaceCode = generateTSInterface(
|
||||||
|
`${pascalCaseName}Emits`,
|
||||||
|
Object.values(componentMeta.events),
|
||||||
|
emitItemHandler,
|
||||||
|
`Emitted events for the ${pascalCaseName} component`
|
||||||
|
)
|
||||||
|
replaceNodeWithPre(node, 'ts', interfaceCode)
|
||||||
|
} else {
|
||||||
|
node[0] = 'p'
|
||||||
|
node[1] = {}
|
||||||
|
node[2] = 'No events available for this component.'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
visitAndReplace(doc, 'component-example', (node) => {
|
||||||
|
const camelName = camelCase(node[1]['name'])
|
||||||
|
const name = camelName.charAt(0).toUpperCase() + camelName.slice(1)
|
||||||
|
const code = components[name].code
|
||||||
|
replaceNodeWithPre(node, 'vue', code, `${name}.vue`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
48
package.json
48
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@nuxt/ui",
|
"name": "@nuxt/ui",
|
||||||
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
|
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
|
||||||
"version": "3.1.3",
|
"version": "3.2.0",
|
||||||
"packageManager": "pnpm@10.12.1",
|
"packageManager": "pnpm@10.12.4",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/nuxt/ui.git"
|
"url": "git+https://github.com/nuxt/ui.git"
|
||||||
@@ -98,9 +98,9 @@
|
|||||||
"prepack": "pnpm build",
|
"prepack": "pnpm build",
|
||||||
"dev": "nuxt dev playground --uiDev",
|
"dev": "nuxt dev playground --uiDev",
|
||||||
"dev:build": "nuxt build playground",
|
"dev:build": "nuxt build playground",
|
||||||
"dev:vue": "vite playground-vue -- --uiDev",
|
"dev:vue": "pnpm --filter playground-vue dev -- --uiDev",
|
||||||
"dev:vue:build": "vite build playground-vue",
|
"dev:vue:build": "pnpm --filter playground-vue build",
|
||||||
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground && nuxt prepare docs && vite build playground-vue",
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground && nuxt prepare docs && pnpm dev:vue:build",
|
||||||
"docs": "nuxt dev docs --uiDev",
|
"docs": "nuxt dev docs --uiDev",
|
||||||
"docs:build": "nuxt build docs",
|
"docs:build": "nuxt build docs",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
@@ -115,17 +115,17 @@
|
|||||||
"@internationalized/date": "^3.8.2",
|
"@internationalized/date": "^3.8.2",
|
||||||
"@internationalized/number": "^3.6.3",
|
"@internationalized/number": "^3.6.3",
|
||||||
"@nuxt/fonts": "^0.11.4",
|
"@nuxt/fonts": "^0.11.4",
|
||||||
"@nuxt/icon": "^1.13.0",
|
"@nuxt/icon": "^1.15.0",
|
||||||
"@nuxt/kit": "^3.17.5",
|
"@nuxt/kit": "4.0.0-rc.0",
|
||||||
"@nuxt/schema": "^3.17.5",
|
"@nuxt/schema": "4.0.0-rc.0",
|
||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tailwindcss/vite": "^4.1.10",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@unhead/vue": "^2.0.10",
|
"@unhead/vue": "^2.0.12",
|
||||||
"@vueuse/core": "^13.3.0",
|
"@vueuse/core": "^13.5.0",
|
||||||
"@vueuse/integrations": "^13.3.0",
|
"@vueuse/integrations": "^13.5.0",
|
||||||
"colortranslator": "^5.0.0",
|
"colortranslator": "^5.0.0",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
@@ -143,31 +143,31 @@
|
|||||||
"mlly": "^1.7.4",
|
"mlly": "^1.7.4",
|
||||||
"ohash": "^2.0.11",
|
"ohash": "^2.0.11",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"reka-ui": "2.3.1",
|
"reka-ui": "2.3.2",
|
||||||
"scule": "^1.3.0",
|
"scule": "^1.3.0",
|
||||||
"tailwind-variants": "^1.0.0",
|
"tailwind-variants": "^1.0.0",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.11",
|
||||||
"tinyglobby": "^0.2.14",
|
"tinyglobby": "^0.2.14",
|
||||||
"unplugin": "^2.3.5",
|
"unplugin": "^2.3.5",
|
||||||
"unplugin-auto-import": "^19.3.0",
|
"unplugin-auto-import": "^19.3.0",
|
||||||
"unplugin-vue-components": "^28.7.0",
|
"unplugin-vue-components": "^28.8.0",
|
||||||
"vaul-vue": "0.4.1",
|
"vaul-vue": "0.4.1",
|
||||||
"vue-component-type-helpers": "^2.2.10"
|
"vue-component-type-helpers": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/eslint-config": "^1.4.1",
|
"@nuxt/eslint-config": "^1.5.2",
|
||||||
"@nuxt/module-builder": "^1.0.1",
|
"@nuxt/module-builder": "^1.0.1",
|
||||||
"@nuxt/test-utils": "^3.19.1",
|
"@nuxt/test-utils": "^3.19.2",
|
||||||
"@release-it/conventional-changelog": "^10.0.1",
|
"@release-it/conventional-changelog": "^10.0.1",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"embla-carousel": "^8.6.0",
|
"embla-carousel": "^8.6.0",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.30.1",
|
||||||
"happy-dom": "^17.6.3",
|
"happy-dom": "^18.0.1",
|
||||||
"nuxt": "^3.17.5",
|
"nuxt": "4.0.0-rc.0",
|
||||||
"release-it": "^19.0.3",
|
"release-it": "^19.0.3",
|
||||||
"vitest": "^3.2.3",
|
"vitest": "^3.2.4",
|
||||||
"vitest-environment-nuxt": "^1.0.1",
|
"vitest-environment-nuxt": "^1.0.1",
|
||||||
"vue-tsc": "^2.2.10"
|
"vue-tsc": "^3.0.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@inertiajs/vue3": "^2.0.7",
|
"@inertiajs/vue3": "^2.0.7",
|
||||||
|
|||||||
@@ -11,14 +11,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/ui": "workspace:*",
|
"@nuxt/ui": "workspace:*",
|
||||||
"vue": "^3.5.16",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"zod": "^3.25.57"
|
"zod": "^3.25.75"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5",
|
"vite": "^7.0.4",
|
||||||
"vue-tsc": "^2.2.10"
|
"vue-tsc": "^3.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,51 @@ const groups = computed(() => [{
|
|||||||
toast.add({ title: 'Label added!' })
|
toast.add({ title: 'Label added!' })
|
||||||
},
|
},
|
||||||
kbds: ['meta', 'L']
|
kbds: ['meta', 'L']
|
||||||
|
}, {
|
||||||
|
label: 'More actions',
|
||||||
|
placeholder: 'Search actions...',
|
||||||
|
children: [{
|
||||||
|
label: 'Create new file',
|
||||||
|
suffix: 'Create a new file in the current directory or workspace.',
|
||||||
|
icon: 'i-lucide-file-plus',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
toast.add({ title: 'New file added!' })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Create new folder',
|
||||||
|
suffix: 'Create a new folder in the current directory or workspace.',
|
||||||
|
icon: 'i-lucide-folder-plus',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
toast.add({ title: 'New folder added!' })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Share',
|
||||||
|
placeholder: 'Search share options...',
|
||||||
|
icon: 'i-lucide-share',
|
||||||
|
children: [{
|
||||||
|
label: 'Share with everyone',
|
||||||
|
suffix: 'Share with everyone in the current directory or workspace.',
|
||||||
|
icon: 'i-lucide-share',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
toast.add({ title: 'Shared with everyone!' })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Share with team',
|
||||||
|
suffix: 'Share with the team in the current directory or workspace.',
|
||||||
|
icon: 'i-lucide-users',
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
toast.add({ title: 'Shared with team!' })
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]
|
||||||
}]
|
}]
|
||||||
}])
|
}])
|
||||||
|
|
||||||
@@ -121,7 +166,27 @@ defineShortcuts({
|
|||||||
multiple
|
multiple
|
||||||
class="sm:max-h-80"
|
class="sm:max-h-80"
|
||||||
@update:model-value="onSelect"
|
@update:model-value="onSelect"
|
||||||
/>
|
>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<UIcon name="i-simple-icons-nuxtdotjs" class="size-5 text-dimmed ml-1" />
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<UButton color="neutral" variant="ghost" label="Open Command" class="text-dimmed" size="xs">
|
||||||
|
<template #trailing>
|
||||||
|
<UKbd value="enter" />
|
||||||
|
</template>
|
||||||
|
</UButton>
|
||||||
|
<USeparator orientation="vertical" class="h-4" />
|
||||||
|
<UButton color="neutral" variant="ghost" label="Actions" class="text-dimmed" size="xs">
|
||||||
|
<template #trailing>
|
||||||
|
<UKbd value="meta" />
|
||||||
|
<UKbd value="k" />
|
||||||
|
</template>
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCommandPalette>
|
||||||
</DefineTemplate>
|
</DefineTemplate>
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col gap-12 w-full max-w-lg">
|
<div class="flex-1 flex flex-col gap-12 w-full max-w-lg">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { h, resolveComponent } from 'vue'
|
|||||||
import { upperFirst } from 'scule'
|
import { upperFirst } from 'scule'
|
||||||
import type { TableColumn, TableRow } from '@nuxt/ui'
|
import type { TableColumn, TableRow } from '@nuxt/ui'
|
||||||
import { getPaginationRowModel } from '@tanstack/vue-table'
|
import { getPaginationRowModel } from '@tanstack/vue-table'
|
||||||
|
import { useClipboard, refDebounced } from '@vueuse/core'
|
||||||
|
|
||||||
const UButton = resolveComponent('UButton')
|
const UButton = resolveComponent('UButton')
|
||||||
const UCheckbox = resolveComponent('UCheckbox')
|
const UCheckbox = resolveComponent('UCheckbox')
|
||||||
@@ -10,6 +11,7 @@ const UBadge = resolveComponent('UBadge')
|
|||||||
const UDropdownMenu = resolveComponent('UDropdownMenu')
|
const UDropdownMenu = resolveComponent('UDropdownMenu')
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const { copy } = useClipboard()
|
||||||
|
|
||||||
type Payment = {
|
type Payment = {
|
||||||
id: string
|
id: string
|
||||||
@@ -145,6 +147,35 @@ const data = ref<Payment[]>([{
|
|||||||
|
|
||||||
const currentID = ref(4601)
|
const currentID = ref(4601)
|
||||||
|
|
||||||
|
function getRowItems(row: TableRow<Payment>) {
|
||||||
|
return [{
|
||||||
|
type: 'label' as const,
|
||||||
|
label: 'Actions'
|
||||||
|
}, {
|
||||||
|
label: 'Copy payment ID',
|
||||||
|
onSelect() {
|
||||||
|
copy(row.original.id)
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
title: 'Payment ID copied to clipboard!',
|
||||||
|
color: 'success',
|
||||||
|
icon: 'i-lucide-circle-check'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: row.getIsExpanded() ? 'Collapse' : 'Expand',
|
||||||
|
onSelect() {
|
||||||
|
row.toggleExpanded()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
type: 'separator' as const
|
||||||
|
}, {
|
||||||
|
label: 'View customer'
|
||||||
|
}, {
|
||||||
|
label: 'View payment details'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
const columns: TableColumn<Payment>[] = [{
|
const columns: TableColumn<Payment>[] = [{
|
||||||
id: 'select',
|
id: 'select',
|
||||||
header: ({ table }) => h(UCheckbox, {
|
header: ({ table }) => h(UCheckbox, {
|
||||||
@@ -211,6 +242,16 @@ const columns: TableColumn<Payment>[] = [{
|
|||||||
}, {
|
}, {
|
||||||
accessorKey: 'amount',
|
accessorKey: 'amount',
|
||||||
header: () => h('div', { class: 'text-right' }, 'Amount'),
|
header: () => h('div', { class: 'text-right' }, 'Amount'),
|
||||||
|
footer: ({ column }) => {
|
||||||
|
const total = column.getFacetedRowModel().rows.reduce((acc: number, row: TableRow<Payment>) => acc + Number.parseFloat(row.getValue('amount')), 0)
|
||||||
|
|
||||||
|
const formatted = new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR'
|
||||||
|
}).format(total)
|
||||||
|
|
||||||
|
return h('div', { class: 'text-right font-medium' }, `Total: ${formatted}`)
|
||||||
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const amount = Number.parseFloat(row.getValue('amount'))
|
const amount = Number.parseFloat(row.getValue('amount'))
|
||||||
|
|
||||||
@@ -225,38 +266,11 @@ const columns: TableColumn<Payment>[] = [{
|
|||||||
id: 'actions',
|
id: 'actions',
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const items = [{
|
|
||||||
type: 'label',
|
|
||||||
label: 'Actions'
|
|
||||||
}, {
|
|
||||||
label: 'Copy payment ID',
|
|
||||||
onSelect() {
|
|
||||||
navigator.clipboard.writeText(row.original.id)
|
|
||||||
|
|
||||||
toast.add({
|
|
||||||
title: 'Payment ID copied to clipboard!',
|
|
||||||
color: 'success',
|
|
||||||
icon: 'i-lucide-circle-check'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
label: row.getIsExpanded() ? 'Collapse' : 'Expand',
|
|
||||||
onSelect() {
|
|
||||||
row.toggleExpanded()
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
type: 'separator'
|
|
||||||
}, {
|
|
||||||
label: 'View customer'
|
|
||||||
}, {
|
|
||||||
label: 'View payment details'
|
|
||||||
}]
|
|
||||||
|
|
||||||
return h('div', { class: 'text-right' }, h(UDropdownMenu, {
|
return h('div', { class: 'text-right' }, h(UDropdownMenu, {
|
||||||
'content': {
|
'content': {
|
||||||
align: 'end'
|
align: 'end'
|
||||||
},
|
},
|
||||||
items,
|
'items': getRowItems(row),
|
||||||
'aria-label': 'Actions dropdown'
|
'aria-label': 'Actions dropdown'
|
||||||
}, () => h(UButton, {
|
}, () => h(UButton, {
|
||||||
'icon': 'i-lucide-ellipsis-vertical',
|
'icon': 'i-lucide-ellipsis-vertical',
|
||||||
@@ -294,8 +308,41 @@ function randomize() {
|
|||||||
data.value = data.value.sort(() => Math.random() - 0.5)
|
data.value = data.value.sort(() => Math.random() - 0.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rowSelection = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
function onSelect(row: TableRow<Payment>) {
|
function onSelect(row: TableRow<Payment>) {
|
||||||
console.log(row)
|
row.toggleSelected(!row.getIsSelected())
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextmenuRow = ref<TableRow<Payment> | null>(null)
|
||||||
|
const contextmenuItems = computed(() => contextmenuRow.value ? getRowItems(contextmenuRow.value) : [])
|
||||||
|
|
||||||
|
function onContextmenu(e: Event, row: TableRow<Payment>) {
|
||||||
|
contextmenuRow.value = row
|
||||||
|
}
|
||||||
|
|
||||||
|
const popoverOpen = ref(false)
|
||||||
|
const popoverOpenDebounced = refDebounced(popoverOpen, 1)
|
||||||
|
const popoverAnchor = ref({ x: 0, y: 0 })
|
||||||
|
const popoverRow = ref<TableRow<Payment> | null>(null)
|
||||||
|
|
||||||
|
const reference = computed(() => ({
|
||||||
|
getBoundingClientRect: () =>
|
||||||
|
({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
left: popoverAnchor.value.x,
|
||||||
|
right: popoverAnchor.value.x,
|
||||||
|
top: popoverAnchor.value.y,
|
||||||
|
bottom: popoverAnchor.value.y,
|
||||||
|
...popoverAnchor.value
|
||||||
|
} as DOMRect)
|
||||||
|
}))
|
||||||
|
|
||||||
|
function onHover(_e: Event, row: TableRow<Payment> | null) {
|
||||||
|
popoverRow.value = row
|
||||||
|
|
||||||
|
popoverOpen.value = !!row
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -342,27 +389,44 @@ onMounted(() => {
|
|||||||
</UDropdownMenu>
|
</UDropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UTable
|
<UContextMenu :items="contextmenuItems">
|
||||||
ref="table"
|
<UTable
|
||||||
:data="data"
|
ref="table"
|
||||||
:columns="columns"
|
:data="data"
|
||||||
:column-pinning="columnPinning"
|
:columns="columns"
|
||||||
:loading="loading"
|
:column-pinning="columnPinning"
|
||||||
:pagination="pagination"
|
:row-selection="rowSelection"
|
||||||
:pagination-options="{
|
:loading="loading"
|
||||||
getPaginationRowModel: getPaginationRowModel()
|
:pagination="pagination"
|
||||||
}"
|
:pagination-options="{
|
||||||
:ui="{
|
getPaginationRowModel: getPaginationRowModel()
|
||||||
tr: 'divide-x divide-default'
|
}"
|
||||||
}"
|
:ui="{
|
||||||
sticky
|
tr: 'divide-x divide-default'
|
||||||
class="border border-accented rounded-sm"
|
}"
|
||||||
@select="onSelect"
|
sticky
|
||||||
>
|
class="border border-accented rounded-sm"
|
||||||
<template #expanded="{ row }">
|
@select="onSelect"
|
||||||
<pre>{{ row.original }}</pre>
|
@contextmenu="onContextmenu"
|
||||||
|
@pointermove="(ev: PointerEvent) => {
|
||||||
|
popoverAnchor.x = ev.clientX
|
||||||
|
popoverAnchor.y = ev.clientY
|
||||||
|
}"
|
||||||
|
@hover="onHover"
|
||||||
|
>
|
||||||
|
<template #expanded="{ row }">
|
||||||
|
<pre>{{ row.original }}</pre>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</UContextMenu>
|
||||||
|
|
||||||
|
<UPopover :content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }" :open="popoverOpenDebounced" :reference="reference">
|
||||||
|
<template #content>
|
||||||
|
<div class="p-4">
|
||||||
|
{{ popoverRow?.original?.id }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UTable>
|
</UPopover>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div class="text-sm text-muted">
|
<div class="text-sm text-muted">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const orientations = Object.keys(theme.variants.orientation)
|
|||||||
const orientation = ref('vertical' as const)
|
const orientation = ref('vertical' as const)
|
||||||
const color = ref('primary' as const)
|
const color = ref('primary' as const)
|
||||||
const size = ref('md' as const)
|
const size = ref('md' as const)
|
||||||
|
const reverse = ref(false)
|
||||||
|
|
||||||
const items = [{
|
const items = [{
|
||||||
date: 'Mar 15, 2025',
|
date: 'Mar 15, 2025',
|
||||||
@@ -46,6 +47,7 @@ const value = ref('kickoff')
|
|||||||
<USelect v-model="orientation" :items="orientations" placeholder="Orientation" />
|
<USelect v-model="orientation" :items="orientations" placeholder="Orientation" />
|
||||||
<USelect v-model="size" :items="sizes" placeholder="Size" />
|
<USelect v-model="size" :items="sizes" placeholder="Size" />
|
||||||
<USelect v-model="value" :items="items.map(item => item.value)" placeholder="Value" />
|
<USelect v-model="value" :items="items.map(item => item.value)" placeholder="Value" />
|
||||||
|
<USelect v-model="reverse" :items="[true, false]" placeholder="Reverse" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UTimeline
|
<UTimeline
|
||||||
@@ -54,6 +56,7 @@ const value = ref('kickoff')
|
|||||||
:orientation="orientation"
|
:orientation="orientation"
|
||||||
:size="size"
|
:size="size"
|
||||||
:items="items"
|
:items="items"
|
||||||
|
:reverse="reverse"
|
||||||
class="data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-96"
|
class="data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-96"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,17 +9,17 @@
|
|||||||
"typecheck": "nuxt typecheck"
|
"typecheck": "nuxt typecheck"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify-json/lucide": "^1.2.47",
|
"@iconify-json/lucide": "^1.2.56",
|
||||||
"@iconify-json/simple-icons": "^1.2.38",
|
"@iconify-json/simple-icons": "^1.2.42",
|
||||||
"@internationalized/date": "^3.8.2",
|
"@internationalized/date": "^3.8.2",
|
||||||
"@nuxt/ui": "workspace:*",
|
"@nuxt/ui": "workspace:*",
|
||||||
"@nuxthub/core": "^0.9.0",
|
"@nuxthub/core": "^0.9.0",
|
||||||
"nuxt": "^3.17.5",
|
"nuxt": "4.0.0-rc.0",
|
||||||
"zod": "^3.25.57"
|
"zod": "^3.25.75"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vue-tsc": "^2.2.10"
|
"vue-tsc": "^3.0.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"unimport": "4.1.1"
|
"unimport": "4.1.1"
|
||||||
|
|||||||
4766
pnpm-lock.yaml
generated
4766
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,12 @@
|
|||||||
"reka-ui",
|
"reka-ui",
|
||||||
"vaul-vue"
|
"vaul-vue"
|
||||||
]
|
]
|
||||||
|
}, {
|
||||||
|
"groupName": "vue-tsc",
|
||||||
|
"matchPackageNames": [
|
||||||
|
"vue-tsc",
|
||||||
|
"vue-component-type-helpers"
|
||||||
|
]
|
||||||
}, {
|
}, {
|
||||||
"matchDepTypes": ["peerDependencies"],
|
"matchDepTypes": ["peerDependencies"],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { name, version } from '../package.json'
|
|||||||
|
|
||||||
export type * from './runtime/types'
|
export type * from './runtime/types'
|
||||||
|
|
||||||
|
type Color = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | (string & {})
|
||||||
|
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | (string & {})
|
||||||
|
|
||||||
export interface ModuleOptions {
|
export interface ModuleOptions {
|
||||||
/**
|
/**
|
||||||
* Prefix for components
|
* Prefix for components
|
||||||
@@ -38,7 +41,7 @@ export interface ModuleOptions {
|
|||||||
* @defaultValue `['primary', 'secondary', 'success', 'info', 'warning', 'error']`
|
* @defaultValue `['primary', 'secondary', 'success', 'info', 'warning', 'error']`
|
||||||
* @link https://ui.nuxt.com/getting-started/installation/nuxt#themecolors
|
* @link https://ui.nuxt.com/getting-started/installation/nuxt#themecolors
|
||||||
*/
|
*/
|
||||||
colors?: string[]
|
colors?: Color[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or disable transitions on components
|
* Enable or disable transitions on components
|
||||||
@@ -46,6 +49,20 @@ export interface ModuleOptions {
|
|||||||
* @link https://ui.nuxt.com/getting-started/installation/nuxt#themetransitions
|
* @link https://ui.nuxt.com/getting-started/installation/nuxt#themetransitions
|
||||||
*/
|
*/
|
||||||
transitions?: boolean
|
transitions?: boolean
|
||||||
|
|
||||||
|
defaultVariants?: {
|
||||||
|
/**
|
||||||
|
* The default color variant to use for components
|
||||||
|
* @defaultValue `'primary'`
|
||||||
|
*/
|
||||||
|
color?: Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default size variant to use for components
|
||||||
|
* @defaultValue `'md'`
|
||||||
|
*/
|
||||||
|
size?: Size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
|
|
||||||
async function registerModule(name: string, key: string, options: Record<string, any>) {
|
async function registerModule(name: string, key: string, options: Record<string, any>) {
|
||||||
if (!hasNuxtModule(name)) {
|
if (!hasNuxtModule(name)) {
|
||||||
await installModule(name, options)
|
await installModule(name, defu((nuxt.options as any)[key], options))
|
||||||
} else {
|
} else {
|
||||||
(nuxt.options as any)[key] = defu((nuxt.options as any)[key], options)
|
(nuxt.options as any)[key] = defu((nuxt.options as any)[key], options)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { defu } from 'defu'
|
|||||||
/**
|
/**
|
||||||
* This plugin adds all the Nuxt UI components as auto-imports.
|
* This plugin adds all the Nuxt UI components as auto-imports.
|
||||||
*/
|
*/
|
||||||
export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix: NonNullable<NuxtUIOptions['prefix']> }, meta: UnpluginContextMeta) {
|
export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix: NonNullable<NuxtUIOptions['prefix']>, extraRuntimeDir?: string }, meta: UnpluginContextMeta) {
|
||||||
const components = globSync('**/*.vue', { cwd: join(runtimeDir, 'components') })
|
const components = globSync('**/*.vue', { cwd: join(runtimeDir, 'components') })
|
||||||
const componentNames = new Set(components.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))
|
const componentNames = new Set(components.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))
|
||||||
|
|
||||||
@@ -50,13 +50,15 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
|
|||||||
name: 'nuxt:ui:components',
|
name: 'nuxt:ui:components',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
resolveId(id, importer) {
|
resolveId(id, importer) {
|
||||||
// only apply to runtime nuxt ui components
|
if (!importer) {
|
||||||
if (!importer || !normalize(importer).includes(runtimeDir)) {
|
return
|
||||||
|
}
|
||||||
|
if (!normalize(importer).includes(runtimeDir) && (!options.extraRuntimeDir || !normalize(importer).includes(options.extraRuntimeDir))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// only apply to relative imports
|
// only apply to relative imports or nuxt ui runtime components
|
||||||
if (!RELATIVE_IMPORT_RE.test(id)) {
|
if (!RELATIVE_IMPORT_RE.test(id) && !id.startsWith('@nuxt/ui/components/')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { normalize } from 'pathe'
|
|||||||
import { resolvePathSync } from 'mlly'
|
import { resolvePathSync } from 'mlly'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
|
|
||||||
import { runtimeDir, type NuxtUIOptions } from '../unplugin'
|
import { runtimeDir } from '../unplugin'
|
||||||
|
import type { NuxtUIOptions } from '../unplugin'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This plugin normalises Nuxt environment (#imports) and `import.meta.client` within the Nuxt UI components.
|
* This plugin normalises Nuxt environment (#imports) and `import.meta.client` within the Nuxt UI components.
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { genSafeVariableName } from 'knitwork'
|
|||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { resolvePathSync } from 'mlly'
|
import { resolvePathSync } from 'mlly'
|
||||||
|
|
||||||
import { runtimeDir, type NuxtUIOptions } from '../unplugin'
|
import { runtimeDir } from '../unplugin'
|
||||||
|
import type { NuxtUIOptions } from '../unplugin'
|
||||||
|
|
||||||
import type { UnpluginOptions } from 'unplugin'
|
import type { UnpluginOptions } from 'unplugin'
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.alert || {})
|
|||||||
<UButton
|
<UButton
|
||||||
v-if="close"
|
v-if="close"
|
||||||
:icon="closeIcon || appConfig.ui.icons.close"
|
:icon="closeIcon || appConfig.ui.icons.close"
|
||||||
size="md"
|
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant="link"
|
variant="link"
|
||||||
:aria-label="t('alert.close')"
|
:aria-label="t('alert.close')"
|
||||||
|
|||||||
@@ -42,14 +42,15 @@ export interface ButtonSlots {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Ref, computed, ref, inject } from 'vue'
|
import { computed, ref, inject } from 'vue'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { useForwardProps } from 'reka-ui'
|
import { useForwardProps } from 'reka-ui'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||||
import { useButtonGroup } from '../composables/useButtonGroup'
|
import { useButtonGroup } from '../composables/useButtonGroup'
|
||||||
import { formLoadingInjectionKey } from '../composables/useFormField'
|
import { formLoadingInjectionKey } from '../composables/useFormField'
|
||||||
import { omit } from '../utils'
|
import { omit, mergeClasses } from '../utils'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import { pickLinkProps } from '../utils/link'
|
import { pickLinkProps } from '../utils/link'
|
||||||
import UIcon from './Icon.vue'
|
import UIcon from './Icon.vue'
|
||||||
@@ -57,11 +58,7 @@ import UAvatar from './Avatar.vue'
|
|||||||
import ULink from './Link.vue'
|
import ULink from './Link.vue'
|
||||||
import ULinkBase from './LinkBase.vue'
|
import ULinkBase from './LinkBase.vue'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
const props = defineProps<ButtonProps>()
|
||||||
active: undefined,
|
|
||||||
activeClass: '',
|
|
||||||
inactiveClass: ''
|
|
||||||
})
|
|
||||||
const slots = defineSlots<ButtonSlots>()
|
const slots = defineSlots<ButtonSlots>()
|
||||||
|
|
||||||
const appConfig = useAppConfig() as Button['AppConfig']
|
const appConfig = useAppConfig() as Button['AppConfig']
|
||||||
@@ -96,10 +93,10 @@ const ui = computed(() => tv({
|
|||||||
variants: {
|
variants: {
|
||||||
active: {
|
active: {
|
||||||
true: {
|
true: {
|
||||||
base: props.activeClass
|
base: mergeClasses(appConfig.ui?.button?.variants?.active?.true?.base, props.activeClass)
|
||||||
},
|
},
|
||||||
false: {
|
false: {
|
||||||
base: props.inactiveClass
|
base: mergeClasses(appConfig.ui?.button?.variants?.active?.false?.base, props.inactiveClass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export interface CarouselEmits {
|
|||||||
import { computed, ref, watch, onMounted } from 'vue'
|
import { computed, ref, watch, onMounted } from 'vue'
|
||||||
import useEmblaCarousel from 'embla-carousel-vue'
|
import useEmblaCarousel from 'embla-carousel-vue'
|
||||||
import { Primitive, useForwardProps } from 'reka-ui'
|
import { Primitive, useForwardProps } from 'reka-ui'
|
||||||
import { reactivePick, computedAsync } from '@vueuse/core'
|
import { reactivePick } from '@vueuse/core'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
import { useLocale } from '../composables/useLocale'
|
import { useLocale } from '../composables/useLocale'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
@@ -175,41 +175,45 @@ const options = computed<EmblaOptionsType>(() => ({
|
|||||||
direction: dir.value === 'rtl' ? 'rtl' : 'ltr'
|
direction: dir.value === 'rtl' ? 'rtl' : 'ltr'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const plugins = computedAsync<EmblaPluginType[]>(async () => {
|
const plugins = ref<EmblaPluginType[]>([])
|
||||||
const plugins = []
|
|
||||||
|
async function loadPlugins() {
|
||||||
|
const emblaPlugins: EmblaPluginType[] = []
|
||||||
|
|
||||||
if (props.autoplay) {
|
if (props.autoplay) {
|
||||||
const AutoplayPlugin = await import('embla-carousel-autoplay').then(r => r.default)
|
const AutoplayPlugin = await import('embla-carousel-autoplay').then(r => r.default)
|
||||||
plugins.push(AutoplayPlugin(typeof props.autoplay === 'boolean' ? {} : props.autoplay))
|
emblaPlugins.push(AutoplayPlugin(typeof props.autoplay === 'boolean' ? {} : props.autoplay))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.autoScroll) {
|
if (props.autoScroll) {
|
||||||
const AutoScrollPlugin = await import('embla-carousel-auto-scroll').then(r => r.default)
|
const AutoScrollPlugin = await import('embla-carousel-auto-scroll').then(r => r.default)
|
||||||
plugins.push(AutoScrollPlugin(typeof props.autoScroll === 'boolean' ? {} : props.autoScroll))
|
emblaPlugins.push(AutoScrollPlugin(typeof props.autoScroll === 'boolean' ? {} : props.autoScroll))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.autoHeight) {
|
if (props.autoHeight) {
|
||||||
const AutoHeightPlugin = await import('embla-carousel-auto-height').then(r => r.default)
|
const AutoHeightPlugin = await import('embla-carousel-auto-height').then(r => r.default)
|
||||||
plugins.push(AutoHeightPlugin(typeof props.autoHeight === 'boolean' ? {} : props.autoHeight))
|
emblaPlugins.push(AutoHeightPlugin(typeof props.autoHeight === 'boolean' ? {} : props.autoHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.classNames) {
|
if (props.classNames) {
|
||||||
const ClassNamesPlugin = await import('embla-carousel-class-names').then(r => r.default)
|
const ClassNamesPlugin = await import('embla-carousel-class-names').then(r => r.default)
|
||||||
plugins.push(ClassNamesPlugin(typeof props.classNames === 'boolean' ? {} : props.classNames))
|
emblaPlugins.push(ClassNamesPlugin(typeof props.classNames === 'boolean' ? {} : props.classNames))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.fade) {
|
if (props.fade) {
|
||||||
const FadePlugin = await import('embla-carousel-fade').then(r => r.default)
|
const FadePlugin = await import('embla-carousel-fade').then(r => r.default)
|
||||||
plugins.push(FadePlugin(typeof props.fade === 'boolean' ? {} : props.fade))
|
emblaPlugins.push(FadePlugin(typeof props.fade === 'boolean' ? {} : props.fade))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.wheelGestures) {
|
if (props.wheelGestures) {
|
||||||
const { WheelGesturesPlugin } = await import('embla-carousel-wheel-gestures')
|
const { WheelGesturesPlugin } = await import('embla-carousel-wheel-gestures')
|
||||||
plugins.push(WheelGesturesPlugin(typeof props.wheelGestures === 'boolean' ? {} : props.wheelGestures))
|
emblaPlugins.push(WheelGesturesPlugin(typeof props.wheelGestures === 'boolean' ? {} : props.wheelGestures))
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugins
|
plugins.value = emblaPlugins
|
||||||
})
|
}
|
||||||
|
|
||||||
|
watch(() => [props.autoplay, props.autoScroll, props.autoHeight, props.classNames, props.fade, props.wheelGestures], loadPlugins, { immediate: true })
|
||||||
|
|
||||||
const [emblaRef, emblaApi] = useEmblaCarousel(options.value, plugins.value)
|
const [emblaRef, emblaApi] = useEmblaCarousel(options.value, plugins.value)
|
||||||
|
|
||||||
@@ -252,6 +256,7 @@ const scrollSnaps = ref<number[]>([])
|
|||||||
function onInit(api: EmblaCarouselType) {
|
function onInit(api: EmblaCarouselType) {
|
||||||
scrollSnaps.value = api?.scrollSnapList() || []
|
scrollSnaps.value = api?.scrollSnapList() || []
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSelect(api: EmblaCarouselType) {
|
function onSelect(api: EmblaCarouselType) {
|
||||||
canScrollNext.value = api?.canScrollNext() || false
|
canScrollNext.value = api?.canScrollNext() || false
|
||||||
canScrollPrev.value = api?.canScrollPrev() || false
|
canScrollPrev.value = api?.canScrollPrev() || false
|
||||||
@@ -296,8 +301,7 @@ defineExpose({
|
|||||||
<div
|
<div
|
||||||
v-for="(item, index) in items"
|
v-for="(item, index) in items"
|
||||||
:key="index"
|
:key="index"
|
||||||
role="group"
|
v-bind="dots ? { role: 'tabpanel' } : { 'role': 'group', 'aria-roledescription': 'slide' }"
|
||||||
aria-roledescription="slide"
|
|
||||||
:class="ui.item({ class: [props.ui?.item, isCarouselItem(item) && item.ui?.item, isCarouselItem(item) && item.class] })"
|
:class="ui.item({ class: [props.ui?.item, isCarouselItem(item) && item.ui?.item, isCarouselItem(item) && item.class] })"
|
||||||
>
|
>
|
||||||
<slot :item="item" :index="index" />
|
<slot :item="item" :index="index" />
|
||||||
@@ -310,7 +314,6 @@ defineExpose({
|
|||||||
<UButton
|
<UButton
|
||||||
:disabled="!canScrollPrev"
|
:disabled="!canScrollPrev"
|
||||||
:icon="prevIcon"
|
:icon="prevIcon"
|
||||||
size="md"
|
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:aria-label="t('carousel.prev')"
|
:aria-label="t('carousel.prev')"
|
||||||
@@ -321,7 +324,6 @@ defineExpose({
|
|||||||
<UButton
|
<UButton
|
||||||
:disabled="!canScrollNext"
|
:disabled="!canScrollNext"
|
||||||
:icon="nextIcon"
|
:icon="nextIcon"
|
||||||
size="md"
|
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:aria-label="t('carousel.next')"
|
:aria-label="t('carousel.next')"
|
||||||
@@ -331,10 +333,13 @@ defineExpose({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="dots" :class="ui.dots({ class: props.ui?.dots })">
|
<div v-if="dots" role="tablist" :aria-label="t('carousel.dots')" :class="ui.dots({ class: props.ui?.dots })">
|
||||||
<template v-for="(_, index) in scrollSnaps" :key="index">
|
<template v-for="(_, index) in scrollSnaps" :key="index">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
:aria-label="t('carousel.goto', { slide: index + 1 })"
|
:aria-label="t('carousel.goto', { slide: index + 1 })"
|
||||||
|
:aria-selected="selectedIndex === index"
|
||||||
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
|
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
|
||||||
:data-state="selectedIndex === index ? 'active' : undefined"
|
:data-state="selectedIndex === index ? 'active' : undefined"
|
||||||
@click="scrollTo(index)"
|
@click="scrollTo(index)"
|
||||||
|
|||||||
@@ -26,13 +26,18 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
|||||||
loading?: boolean
|
loading?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
slot?: string
|
slot?: string
|
||||||
|
/**
|
||||||
|
* The placeholder to display when the item has children.
|
||||||
|
*/
|
||||||
|
placeholder?: string
|
||||||
|
children?: CommandPaletteItem[]
|
||||||
onSelect?(e?: Event): void
|
onSelect?(e?: Event): void
|
||||||
class?: any
|
class?: any
|
||||||
ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'>
|
ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'>
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommandPaletteGroup<T> {
|
export interface CommandPaletteGroup<T extends CommandPaletteItem = CommandPaletteItem> {
|
||||||
id: string
|
id: string
|
||||||
label?: string
|
label?: string
|
||||||
slot?: string
|
slot?: string
|
||||||
@@ -52,7 +57,7 @@ export interface CommandPaletteGroup<T> {
|
|||||||
highlightedIcon?: string
|
highlightedIcon?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multiple' | 'disabled' | 'modelValue' | 'defaultValue' | 'highlightOnHover'>, Pick<UseComponentIconsProps, 'loading' | 'loadingIcon'> {
|
export interface CommandPaletteProps<G extends CommandPaletteGroup<T> = CommandPaletteGroup<any>, T extends CommandPaletteItem = CommandPaletteItem> extends Pick<ListboxRootProps, 'multiple' | 'disabled' | 'modelValue' | 'defaultValue' | 'highlightOnHover' | 'selectionBehavior'>, Pick<UseComponentIconsProps, 'loading' | 'loadingIcon'> {
|
||||||
/**
|
/**
|
||||||
* The element or component this component should render as.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'div'
|
* @defaultValue 'div'
|
||||||
@@ -70,6 +75,12 @@ export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multi
|
|||||||
* @IconifyIcon
|
* @IconifyIcon
|
||||||
*/
|
*/
|
||||||
selectedIcon?: string
|
selectedIcon?: string
|
||||||
|
/**
|
||||||
|
* The icon displayed when an item has children.
|
||||||
|
* @defaultValue appConfig.ui.icons.chevronRight
|
||||||
|
* @IconifyIcon
|
||||||
|
*/
|
||||||
|
trailingIcon?: string
|
||||||
/**
|
/**
|
||||||
* The placeholder text for the input.
|
* The placeholder text for the input.
|
||||||
* @defaultValue t('commandPalette.placeholder')
|
* @defaultValue t('commandPalette.placeholder')
|
||||||
@@ -93,6 +104,18 @@ export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multi
|
|||||||
* @IconifyIcon
|
* @IconifyIcon
|
||||||
*/
|
*/
|
||||||
closeIcon?: string
|
closeIcon?: string
|
||||||
|
/**
|
||||||
|
* Display a button to navigate back in history.
|
||||||
|
* `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
|
||||||
|
* @defaultValue true
|
||||||
|
*/
|
||||||
|
back?: boolean | ButtonProps
|
||||||
|
/**
|
||||||
|
* The icon displayed in the back button.
|
||||||
|
* @defaultValue appConfig.ui.icons.arrowLeft
|
||||||
|
* @IconifyIcon
|
||||||
|
*/
|
||||||
|
backIcon?: string
|
||||||
groups?: G[]
|
groups?: G[]
|
||||||
/**
|
/**
|
||||||
* Options for [useFuse](https://vueuse.org/integrations/useFuse).
|
* Options for [useFuse](https://vueuse.org/integrations/useFuse).
|
||||||
@@ -116,14 +139,16 @@ export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multi
|
|||||||
ui?: CommandPalette['slots']
|
ui?: CommandPalette['slots']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommandPaletteEmits<T> = ListboxRootEmits<T> & {
|
export type CommandPaletteEmits<T extends CommandPaletteItem = CommandPaletteItem> = ListboxRootEmits<T> & {
|
||||||
'update:open': [value: boolean]
|
'update:open': [value: boolean]
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||||
|
|
||||||
export type CommandPaletteSlots<G extends { slot?: string }, T extends { slot?: string }> = {
|
export type CommandPaletteSlots<G extends CommandPaletteGroup<T> = CommandPaletteGroup<any>, T extends CommandPaletteItem = CommandPaletteItem> = {
|
||||||
'empty'(props: { searchTerm?: string }): any
|
'empty'(props: { searchTerm?: string }): any
|
||||||
|
'footer'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
|
||||||
|
'back'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
|
||||||
'close'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
|
'close'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
|
||||||
'item': SlotProps<T>
|
'item': SlotProps<T>
|
||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
@@ -134,7 +159,7 @@ export type CommandPaletteSlots<G extends { slot?: string }, T extends { slot?:
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="G extends CommandPaletteGroup<T>, T extends CommandPaletteItem">
|
<script setup lang="ts" generic="G extends CommandPaletteGroup<T>, T extends CommandPaletteItem">
|
||||||
import { computed } from 'vue'
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
import { ListboxRoot, ListboxFilter, ListboxContent, ListboxGroup, ListboxGroupLabel, ListboxItem, ListboxItemIndicator, useForwardProps, useForwardPropsEmits } from 'reka-ui'
|
import { ListboxRoot, ListboxFilter, ListboxContent, ListboxGroup, ListboxGroupLabel, ListboxItem, ListboxItemIndicator, useForwardProps, useForwardPropsEmits } from 'reka-ui'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { reactivePick } from '@vueuse/core'
|
import { reactivePick } from '@vueuse/core'
|
||||||
@@ -157,7 +182,8 @@ import UKbd from './Kbd.vue'
|
|||||||
const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
|
const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
|
||||||
modelValue: '',
|
modelValue: '',
|
||||||
labelKey: 'label',
|
labelKey: 'label',
|
||||||
autofocus: true
|
autofocus: true,
|
||||||
|
back: true
|
||||||
})
|
})
|
||||||
const emits = defineEmits<CommandPaletteEmits<T>>()
|
const emits = defineEmits<CommandPaletteEmits<T>>()
|
||||||
const slots = defineSlots<CommandPaletteSlots<G, T>>()
|
const slots = defineSlots<CommandPaletteSlots<G, T>>()
|
||||||
@@ -167,7 +193,7 @@ const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
|||||||
const { t } = useLocale()
|
const { t } = useLocale()
|
||||||
const appConfig = useAppConfig() as CommandPalette['AppConfig']
|
const appConfig = useAppConfig() as CommandPalette['AppConfig']
|
||||||
|
|
||||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'highlightOnHover'), emits)
|
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'highlightOnHover', 'selectionBehavior'), emits)
|
||||||
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon'))
|
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon'))
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
@@ -183,18 +209,22 @@ const fuse = computed(() => defu({}, props.fuse, {
|
|||||||
matchAllWhenSearchEmpty: true
|
matchAllWhenSearchEmpty: true
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const items = computed(() => props.groups?.filter((group) => {
|
const history = ref<(CommandPaletteGroup & { placeholder?: string })[]>([])
|
||||||
|
|
||||||
|
const placeholder = computed(() => history.value[history.value.length - 1]?.placeholder || props.placeholder || t('commandPalette.placeholder'))
|
||||||
|
|
||||||
|
const groups = computed(() => history.value?.length ? [history.value[history.value.length - 1] as G] : props.groups)
|
||||||
|
|
||||||
|
const items = computed(() => groups.value?.filter((group) => {
|
||||||
if (!group.id) {
|
if (!group.id) {
|
||||||
console.warn(`[@nuxt/ui] CommandPalette group is missing an \`id\` property`)
|
console.warn(`[@nuxt/ui] CommandPalette group is missing an \`id\` property`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group.ignoreFilter) {
|
if (group.ignoreFilter) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}).flatMap(group => group.items?.map(item => ({ ...item, group: group.id })) || []) || [])
|
})?.flatMap(group => group.items?.map(item => ({ ...item, group: group.id })) || []) || [])
|
||||||
|
|
||||||
const { results: fuseResults } = useFuse<typeof items.value[number]>(searchTerm, items, fuse)
|
const { results: fuseResults } = useFuse<typeof items.value[number]>(searchTerm, items, fuse)
|
||||||
|
|
||||||
@@ -215,7 +245,7 @@ function getGroupWithItems(group: G, items: (T & { matches?: FuseResult<T>['matc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = computed(() => {
|
const filteredGroups = computed(() => {
|
||||||
const groupsById = fuseResults.value.reduce((acc, result) => {
|
const groupsById = fuseResults.value.reduce((acc, result) => {
|
||||||
const { item, matches } = result
|
const { item, matches } = result
|
||||||
if (!item.group) {
|
if (!item.group) {
|
||||||
@@ -229,7 +259,7 @@ const groups = computed(() => {
|
|||||||
}, {} as Record<string, (T & { matches?: FuseResult<T>['matches'] })[]>)
|
}, {} as Record<string, (T & { matches?: FuseResult<T>['matches'] })[]>)
|
||||||
|
|
||||||
const fuseGroups = Object.entries(groupsById).map(([id, items]) => {
|
const fuseGroups = Object.entries(groupsById).map(([id, items]) => {
|
||||||
const group = props.groups?.find(group => group.id === id)
|
const group = groups.value?.find(group => group.id === id)
|
||||||
if (!group) {
|
if (!group) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -237,7 +267,7 @@ const groups = computed(() => {
|
|||||||
return getGroupWithItems(group, items)
|
return getGroupWithItems(group, items)
|
||||||
}).filter(group => !!group)
|
}).filter(group => !!group)
|
||||||
|
|
||||||
const nonFuseGroups = props.groups
|
const nonFuseGroups = groups.value
|
||||||
?.map((group, index) => ({ ...group, index }))
|
?.map((group, index) => ({ ...group, index }))
|
||||||
?.filter(group => group.ignoreFilter && group.items?.length)
|
?.filter(group => group.ignoreFilter && group.items?.length)
|
||||||
?.map(group => ({ ...getGroupWithItems(group, group.items || []), index: group.index })) || []
|
?.map(group => ({ ...getGroupWithItems(group, group.items || []), index: group.index })) || []
|
||||||
@@ -247,26 +277,88 @@ const groups = computed(() => {
|
|||||||
return acc
|
return acc
|
||||||
}, [...fuseGroups])
|
}, [...fuseGroups])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const listboxRootRef = useTemplateRef('listboxRootRef')
|
||||||
|
|
||||||
|
function navigate(item: T) {
|
||||||
|
if (!item.children?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
history.value.push({
|
||||||
|
id: `history-${history.value.length}`,
|
||||||
|
label: item.label,
|
||||||
|
slot: item.slot,
|
||||||
|
placeholder: item.placeholder,
|
||||||
|
items: item.children
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
searchTerm.value = ''
|
||||||
|
|
||||||
|
listboxRootRef.value?.highlightFirstItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateBack() {
|
||||||
|
if (!history.value.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
history.value.pop()
|
||||||
|
|
||||||
|
searchTerm.value = ''
|
||||||
|
|
||||||
|
listboxRootRef.value?.highlightFirstItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBackspace() {
|
||||||
|
if (!searchTerm.value) {
|
||||||
|
navigateBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelect(e: Event, item: T) {
|
||||||
|
if (item.children?.length) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
navigate(item)
|
||||||
|
} else {
|
||||||
|
item.onSelect?.(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<ListboxRoot v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
<ListboxRoot v-bind="rootProps" ref="listboxRootRef" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||||
<ListboxFilter v-model="searchTerm" as-child>
|
<ListboxFilter v-model="searchTerm" as-child>
|
||||||
<UInput
|
<UInput
|
||||||
:placeholder="placeholder || t('commandPalette.placeholder')"
|
:placeholder="placeholder"
|
||||||
variant="none"
|
variant="none"
|
||||||
:autofocus="autofocus"
|
:autofocus="autofocus"
|
||||||
v-bind="inputProps"
|
v-bind="inputProps"
|
||||||
:icon="icon || appConfig.ui.icons.search"
|
:icon="icon || appConfig.ui.icons.search"
|
||||||
:class="ui.input({ class: props.ui?.input })"
|
:class="ui.input({ class: props.ui?.input })"
|
||||||
|
@keydown.backspace="onBackspace"
|
||||||
>
|
>
|
||||||
|
<template v-if="history?.length && (back || !!slots.back)" #leading>
|
||||||
|
<slot name="back" :ui="ui">
|
||||||
|
<UButton
|
||||||
|
:icon="backIcon || appConfig.ui.icons.arrowLeft"
|
||||||
|
color="neutral"
|
||||||
|
variant="link"
|
||||||
|
:aria-label="t('commandPalette.back')"
|
||||||
|
v-bind="(typeof back === 'object' ? back as Partial<ButtonProps> : {})"
|
||||||
|
:class="ui.back({ class: props.ui?.back })"
|
||||||
|
@click="navigateBack"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-if="close || !!slots.close" #trailing>
|
<template v-if="close || !!slots.close" #trailing>
|
||||||
<slot name="close" :ui="ui">
|
<slot name="close" :ui="ui">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="close"
|
v-if="close"
|
||||||
:icon="closeIcon || appConfig.ui.icons.close"
|
:icon="closeIcon || appConfig.ui.icons.close"
|
||||||
size="md"
|
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:aria-label="t('commandPalette.close')"
|
:aria-label="t('commandPalette.close')"
|
||||||
@@ -280,8 +372,8 @@ const groups = computed(() => {
|
|||||||
</ListboxFilter>
|
</ListboxFilter>
|
||||||
|
|
||||||
<ListboxContent :class="ui.content({ class: props.ui?.content })">
|
<ListboxContent :class="ui.content({ class: props.ui?.content })">
|
||||||
<div v-if="groups?.length" role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
|
<div v-if="filteredGroups?.length" role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
|
||||||
<ListboxGroup v-for="group in groups" :key="`group-${group.id}`" :class="ui.group({ class: props.ui?.group })">
|
<ListboxGroup v-for="group in filteredGroups" :key="`group-${group.id}`" :class="ui.group({ class: props.ui?.group })">
|
||||||
<ListboxGroupLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
|
<ListboxGroupLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
|
||||||
{{ get(group, props.labelKey as string) }}
|
{{ get(group, props.labelKey as string) }}
|
||||||
</ListboxGroupLabel>
|
</ListboxGroupLabel>
|
||||||
@@ -289,10 +381,10 @@ const groups = computed(() => {
|
|||||||
<ListboxItem
|
<ListboxItem
|
||||||
v-for="(item, index) in group.items"
|
v-for="(item, index) in group.items"
|
||||||
:key="`group-${group.id}-${index}`"
|
:key="`group-${group.id}-${index}`"
|
||||||
:value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml'])"
|
:value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml', 'children'])"
|
||||||
:disabled="item.disabled"
|
:disabled="item.disabled"
|
||||||
as-child
|
as-child
|
||||||
@select="item.onSelect"
|
@select="onSelect($event, item)"
|
||||||
>
|
>
|
||||||
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
||||||
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class], active: active || item.active })">
|
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class], active: active || item.active })">
|
||||||
@@ -323,13 +415,20 @@ const groups = computed(() => {
|
|||||||
|
|
||||||
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, item.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">
|
<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, item.ui?.itemTrailingKbds] })">
|
<UIcon
|
||||||
|
v-if="item.children && item.children.length > 0"
|
||||||
|
:name="trailingIcon || appConfig.ui.icons.chevronRight"
|
||||||
|
:class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span v-else-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" />
|
<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>
|
</span>
|
||||||
|
|
||||||
<UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: [props.ui?.itemTrailingHighlightedIcon, item.ui?.itemTrailingHighlightedIcon] })" />
|
<UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: [props.ui?.itemTrailingHighlightedIcon, item.ui?.itemTrailingHighlightedIcon] })" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<ListboxItemIndicator as-child>
|
<ListboxItemIndicator v-if="!item.children?.length" as-child>
|
||||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })" />
|
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })" />
|
||||||
</ListboxItemIndicator>
|
</ListboxItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
@@ -346,5 +445,9 @@ const groups = computed(() => {
|
|||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</ListboxContent>
|
</ListboxContent>
|
||||||
|
|
||||||
|
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
|
||||||
|
<slot name="footer" :ui="ui" />
|
||||||
|
</div>
|
||||||
</ListboxRoot>
|
</ListboxRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import ULink from './Link.vue'
|
|||||||
import UAvatar from './Avatar.vue'
|
import UAvatar from './Avatar.vue'
|
||||||
import UIcon from './Icon.vue'
|
import UIcon from './Icon.vue'
|
||||||
import UKbd from './Kbd.vue'
|
import UKbd from './Kbd.vue'
|
||||||
// eslint-disable-next-line import/no-self-import
|
|
||||||
import UContextMenuContent from './ContextMenuContent.vue'
|
import UContextMenuContent from './ContextMenuContent.vue'
|
||||||
|
|
||||||
const props = defineProps<ContextMenuContentProps<T>>()
|
const props = defineProps<ContextMenuContentProps<T>>()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user