Compare commits

..

4 Commits

Author SHA1 Message Date
Benjamin Canac
16a4b0bbb1 up 2025-05-15 16:48:25 +02:00
Benjamin Canac
dd34d49cd9 Merge branch 'v3' into docs/ai-search 2025-05-15 16:47:37 +02:00
Benjamin Canac
1820e3cc6f up 2025-05-09 19:10:05 +02:00
Benjamin Canac
450948646a docs(app): implement AI search 2025-05-09 18:26:28 +02:00
184 changed files with 11978 additions and 13479 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @benjamincanac

View File

@@ -1,40 +1,5 @@
# Changelog
## [3.1.3](https://github.com/nuxt/ui/compare/v3.1.2...v3.1.3) (2025-05-26)
### ⚠ BREAKING CHANGES
* **NavigationMenu:** revert new `collapsible` field
### Features
* **locale:** add Kyrgyz language ([#4189](https://github.com/nuxt/ui/issues/4189)) ([4053047](https://github.com/nuxt/ui/commit/405304775e4b2b4e8b37a2364f3e5ee34b46036e))
* **locale:** add Lithuanian language ([#4171](https://github.com/nuxt/ui/issues/4171)) ([d86956e](https://github.com/nuxt/ui/commit/d86956e1d57482b3e98eef2d34bff13544284b0b))
* **locale:** add Malay language ([#4160](https://github.com/nuxt/ui/issues/4160)) ([c00f6e8](https://github.com/nuxt/ui/commit/c00f6e8cdfd88eeba58812b78d94a2326c13f164))
* **locale:** add Mongolian language ([#4214](https://github.com/nuxt/ui/issues/4214)) ([44ea02c](https://github.com/nuxt/ui/commit/44ea02c0d64322ef0cfda63b234369c00d3d0180))
* **Modal/Slideover:** add `after:enter` event ([#4187](https://github.com/nuxt/ui/issues/4187)) ([d9e9fea](https://github.com/nuxt/ui/commit/d9e9fea35e4b22d68324c9e85b3aa221a7987d0f))
* **NavigationMenu:** add `tooltip` and `popover` props ([f2682fd](https://github.com/nuxt/ui/commit/f2682fd2ae8abb7807977727fc22ef34cb5752e5)), closes [#4186](https://github.com/nuxt/ui/issues/4186)
* **NavigationMenu:** add `trigger` type in items ([9cf9f25](https://github.com/nuxt/ui/commit/9cf9f25f4424447691e03e9034155d1541badd43))
* **NavigationMenu:** handle `vertical` orientation with Accordion instead of Collapsible ([1e2a10b](https://github.com/nuxt/ui/commit/1e2a10b4bdebaef12316ac60f98a956dad21c1ec)), closes [#4072](https://github.com/nuxt/ui/issues/4072) [#3911](https://github.com/nuxt/ui/issues/3911)
* **Popover:** add `anchor` slot ([#4119](https://github.com/nuxt/ui/issues/4119)) ([473513c](https://github.com/nuxt/ui/commit/473513c2460d4329d7d2e0a0ea69bf1310a072d1))
### Bug Fixes
* **CheckboxGroup/RadioGroup:** variant `table` borders in RTL mode ([#4192](https://github.com/nuxt/ui/issues/4192)) ([43d281f](https://github.com/nuxt/ui/commit/43d281f6d1d8b0017ed61d929c5e311fb5b03447))
* **CommandPalette:** add `presentation` role to viewport ([2ba94db](https://github.com/nuxt/ui/commit/2ba94db09e1ba86020d5d289f1ca1e24ef706299))
* **ContextMenu/DropdownMenu:** wrap groups in a viewport ([dcf34a7](https://github.com/nuxt/ui/commit/dcf34a7ac236b96b1302ec2eae155b8f2d3784ef)), closes [#3315](https://github.com/nuxt/ui/issues/3315)
* **Drawer:** improve title & description accessibility ([41087d4](https://github.com/nuxt/ui/commit/41087d4c9569eb00c04bd748e055cd151c2f762c)), closes [#4199](https://github.com/nuxt/ui/issues/4199)
* **icons:** update `loading` icon ([#4163](https://github.com/nuxt/ui/issues/4163)) ([fe4e1f8](https://github.com/nuxt/ui/commit/fe4e1f859d42aa3c32bb7b75302e84a280abe525))
* **Input/Textarea:** define model modifiers types ([#4195](https://github.com/nuxt/ui/issues/4195)) ([3243fb8](https://github.com/nuxt/ui/commit/3243fb88f71c5475824bfdc4d7c4f303b2d6790b))
* **InputMenu/Select/SelectMenu:** manual viewport to display scrollbars ([f95abf8](https://github.com/nuxt/ui/commit/f95abf8d1d7b9149e400d7dc6f96f93f5154da7a)), closes [#4069](https://github.com/nuxt/ui/issues/4069)
* **NavigationMenu:** incorrect hover when disabled and active ([d0be599](https://github.com/nuxt/ui/commit/d0be59946bfe30c79a6f75476385ab8538aa51b8))
* **NavigationMenu:** only display `tooltip` when collapsed ([44f536f](https://github.com/nuxt/ui/commit/44f536fd0034facb3550d910fae71d4f9442ed19))
* **NavigationMenu:** remove `font-medium` in popover children ([0236399](https://github.com/nuxt/ui/commit/02363994d66d3c2d11b9913f31167fa25f5c5de2))
* **NavigationMenu:** revert new `collapsible` field ([3c78e2f](https://github.com/nuxt/ui/commit/3c78e2fd983f19b5cec65b4a94a8a8b14e548e5e))
* **Textarea:** missing imports ([#4207](https://github.com/nuxt/ui/issues/4207)) ([6aab62e](https://github.com/nuxt/ui/commit/6aab62ec30e266c5f0da0cd24aefbb7c53f447ac))
* **theme:** define `old-neutral` color as static ([#4193](https://github.com/nuxt/ui/issues/4193)) ([dae9f0b](https://github.com/nuxt/ui/commit/dae9f0b8631b3b9fb60ef47753f7aded0c36c4a2))
* **Tooltip:** increase padding for consistency ([0634a75](https://github.com/nuxt/ui/commit/0634a756a496f5131841abafd218ae7e4aaa61e5))
## [3.1.2](https://github.com/nuxt/ui/compare/v3.1.1...v3.1.2) (2025-05-15)
### Features

View File

@@ -12,7 +12,6 @@ const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSe
})
const links = useLinks()
const searchLinks = useSearchLinks()
const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white')
const radius = computed(() => `:root { --ui-radius: ${appConfig.theme.radius}rem; }`)
const blackAsPrimary = computed(() => appConfig.theme.blackAsPrimary ? `:root { --ui-primary: black; } .dark { --ui-primary: white; }` : ':root {}')
@@ -42,7 +41,6 @@ useServerSeoMeta({
useFaviconFromTheme()
const { frameworks, modules } = useSharedData()
const { mappedNavigation, filteredNavigation } = useContentNavigation(navigation)
provide('navigation', mappedNavigation)
@@ -65,23 +63,7 @@ provide('navigation', mappedNavigation)
<template v-if="!route.path.startsWith('/examples')">
<Footer />
<ClientOnly>
<LazyUContentSearch
:links="searchLinks"
:files="files"
:groups="[{
id: 'framework',
label: 'Framework',
items: frameworks
}, {
id: 'module',
label: 'Module',
items: modules
}]"
:navigation="filteredNavigation"
:fuse="{ resultLimit: 100 }"
/>
</ClientOnly>
<Search :files="files" :navigation="filteredNavigation" />
</template>
</UApp>
</template>

View File

@@ -0,0 +1,199 @@
<script setup lang="ts">
import type { DefineComponent } from 'vue'
import type { ContentNavigationItem } from '@nuxt/content'
import { useChat } from '@ai-sdk/vue'
import ProseStreamPre from './prose/PreStream.vue'
const components = {
pre: ProseStreamPre as unknown as DefineComponent
}
interface ContentSearchFile {
id: string
title: string
titles: string[]
level: number
content: string
}
defineProps<{
files?: ContentSearchFile[]
navigation?: ContentNavigationItem[]
}>()
const { frameworks, modules } = useSharedData()
const { messages, input, handleSubmit, status, error, reload, setMessages } = useChat({
maxSteps: 2
})
const ai = ref(false)
const searchTerm = ref('')
const links = computed(() => [{
label: 'Ask AI',
icon: 'i-lucide-bot',
onSelect: (e: any) => {
e.preventDefault()
ai.value = true
}
}, {
label: 'Docs',
icon: 'i-lucide-square-play',
to: '/getting-started'
}, {
label: 'Components',
icon: 'i-lucide-square-code',
to: '/components'
}, {
icon: 'i-lucide-sparkles',
label: 'Pro > Features',
description: 'A collection of premium Vue components.',
to: '/pro'
}, {
icon: 'i-lucide-credit-card',
label: 'Pro > Pricing',
description: 'Free in development, buy when ready to launch.',
to: '/pro/pricing'
}, {
icon: 'i-lucide-panels-top-left',
label: 'Pro > Templates',
description: 'Official templates made with Nuxt UI Pro.',
to: '/pro/templates'
}, {
icon: 'i-lucide-circle-check',
label: 'Pro > Activate',
description: 'Enable Nuxt UI Pro in your production projects.',
to: '/pro/activate'
}, {
label: 'Figma',
icon: 'i-simple-icons-figma',
to: '/figma'
}, {
icon: 'i-lucide-presentation',
label: 'Community > Showcase',
description: 'Check out some of the amazing projects built with Nuxt UI.',
to: '/showcase'
}, {
label: 'Community > Contribution',
description: 'A comprehensive guide on contributing to Nuxt UI, including project structure, development workflow, and best practices.',
icon: 'i-lucide-git-pull-request-arrow',
to: '/getting-started/contribution'
}, {
label: 'Community > Roadmap',
description: 'Track our development progress in real-time.',
icon: 'i-lucide-map',
to: '/roadmap'
}, {
label: 'Community > Devtools',
description: 'Integrate Nuxt UI with Nuxt Devtools with Compodium.',
icon: 'i-lucide-code',
to: 'https://github.com/romhml/compodium',
target: '_blank'
}, {
label: 'Community > Team',
description: 'Meet the team behind Nuxt UI.',
icon: 'i-lucide-users',
to: '/team'
}, {
label: 'Releases',
icon: 'i-lucide-rocket',
to: 'https://github.com/nuxt/ui/releases',
target: '_blank'
}])
const groups = computed(() => [{
id: 'ai',
label: 'AI',
ignoreFilter: true,
items: [{
label: searchTerm.value ? `Ask Nuxt AI for “${searchTerm.value}` : 'Ask Nuxt AI',
icon: 'i-lucide-bot',
onSelect: (e: any) => {
e.preventDefault()
ai.value = true
if (searchTerm.value) {
setMessages([{
id: '1',
role: 'user',
content: searchTerm.value
}])
reload()
}
}
}]
}, {
id: 'framework',
label: 'Framework',
items: frameworks.value
}, {
id: 'module',
label: 'Module',
items: modules.value
}])
function onClose(e: Event) {
console.log('onClose')
e.preventDefault()
ai.value = false
}
</script>
<template>
<LazyUContentSearch
v-model:search-term="searchTerm"
:links="links"
:files="files"
:groups="groups"
:navigation="navigation"
:fuse="{ resultLimit: 100 }"
>
<template v-if="ai" #content>
<UChatPalette>
<UChatMessages
:messages="messages"
:status="status"
:user="{ side: 'left', variant: 'naked', icon: 'i-lucide-user' }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<MDCCached
v-if="message.toolInvocations?.[0]?.state === 'result'"
:value="message.toolInvocations?.[0]?.result"
:cache-key="message.id"
unwrap="p"
:components="components"
:parser-options="{ highlight: false }"
/>
<MDCCached
v-else-if="message.content.length > 0"
:value="message.content"
:cache-key="message.id"
unwrap="p"
:components="components"
:parser-options="{ highlight: false }"
/>
<span v-else class="italic font-light">
Searching documentation...
</span>
</template>
</UChatMessages>
<template #prompt>
<UChatPrompt
v-model="input"
icon="i-lucide-search"
variant="naked"
:error="error"
@submit="handleSubmit"
@close="onClose"
/>
</template>
</UChatPalette>
</template>
</LazyUContentSearch>
</template>

View File

@@ -25,8 +25,6 @@ function getEmojiFlag(locale: string): string {
kk: 'kz', // Kazakh -> Kazakhstan
km: 'kh', // Khmer -> Cambodia
ko: 'kr', // Korean -> South Korea
ky: 'kg', // Kyrgyz -> Kyrgyzstan
ms: 'my', // Malay -> Malaysia
nb: 'no', // Norwegian Bokmål -> Norway
sl: 'si', // Slovenian -> Slovenia
sv: 'se', // Swedish -> Sweden

View File

@@ -28,7 +28,7 @@ const items = [
</template>
<template #refresh-trailing>
<UIcon v-if="loading" name="i-lucide-loader-circle" class="shrink-0 size-5 text-primary animate-spin" />
<UIcon v-if="loading" name="i-lucide-refresh-cw" class="shrink-0 size-5 text-primary animate-spin" />
</template>
</UContextMenu>
</template>

View File

@@ -3,7 +3,7 @@ const open = ref(false)
</script>
<template>
<UDrawer v-model:open="open" :dismissible="false" :handle="false" :ui="{ header: 'flex items-center justify-between' }">
<UDrawer v-model:open="open" :dismissible="false" :ui="{ header: 'flex items-center justify-between' }">
<UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
<template #header>

View File

@@ -1,28 +0,0 @@
<script setup lang="ts">
const open = ref(false)
</script>
<template>
<UDrawer
v-model:open="open"
:dismissible="false"
:overlay="false"
:handle="false"
:modal="false"
:ui="{ header: 'flex items-center justify-between' }"
>
<UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
<template #header>
<h2 class="text-highlighted font-semibold">
Drawer non-dismissible
</h2>
<UButton color="neutral" variant="ghost" icon="i-lucide-x" @click="open = false" />
</template>
<template #body>
<Placeholder class="h-48" />
</template>
</UDrawer>
</template>

View File

@@ -15,9 +15,6 @@ const schema = z.object({
select: z.string().refine(value => value === 'option-2', {
message: 'Select Option 2'
}),
selectMultiple: z.array(z.string()).refine(values => values.includes('option-2'), {
message: 'Include Option 2'
}),
selectMenu: z.any().refine(option => option?.value === 'option-2', {
message: 'Select Option 2'
}),
@@ -84,10 +81,6 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
<USelect v-model="state.select" :items="items" class="w-full" />
</UFormField>
<UFormField name="selectMultiple" label="Select (Multiple)">
<USelect v-model="state.selectMultiple" multiple :items="items" class="w-full" />
</UFormField>
<UFormField name="selectMenu" label="Select Menu">
<USelectMenu v-model="state.selectMenu" :items="items" class="w-full" />
</UFormField>

View File

@@ -10,7 +10,7 @@ const domain = ref(domains[0])
v-model="value"
placeholder="nuxt"
:ui="{
base: 'pl-14.5',
base: 'pl-[57px]',
leading: 'pointer-events-none'
}"
>

View File

@@ -10,8 +10,8 @@ const open = ref(false)
<Placeholder class="h-48" />
</template>
<template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<template #footer>
<UButton label="Cancel" color="neutral" variant="outline" @click="open = false" />
<UButton label="Submit" color="neutral" />
</template>
</UModal>

View File

@@ -1,19 +0,0 @@
<script lang="ts" setup>
const open = ref(false)
</script>
<template>
<UPopover
v-model:open="open"
:dismissible="false"
:ui="{ content: 'w-(--reka-popper-anchor-width) p-4' }"
>
<template #anchor>
<UInput placeholder="Focus to open" @focus="open = true" @blur="open = false" />
</template>
<template #content>
<Placeholder class="w-full aspect-square" />
</template>
</UPopover>
</template>

View File

@@ -10,8 +10,8 @@ const open = ref(false)
<Placeholder class="h-full" />
</template>
<template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<template #footer>
<UButton label="Cancel" color="neutral" variant="outline" @click="open = false" />
<UButton label="Submit" color="neutral" />
</template>
</USlideover>

View File

@@ -1,34 +0,0 @@
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'
const items: TimelineItem[] = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
icon: 'i-lucide-code',
value: 'development'
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
icon: 'i-lucide-check-circle',
value: 'deployment'
}]
</script>
<template>
<UTimeline
:items="items"
:ui="{ item: 'even:flex-row-reverse even:-translate-x-[calc(100%-2rem)] even:text-right' }"
:default-value="2"
class="w-full translate-x-[calc(50%-2rem)]"
/>
</template>

View File

@@ -1,52 +0,0 @@
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'
const items = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
subtitle: 'Project Initiation',
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
icon: 'i-lucide-code',
value: 'development',
slot: 'development' as const,
developers: [
{
src: 'https://github.com/J-Michalek.png'
}, {
src: 'https://github.com/benjamincanac.png'
}
]
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
description: 'QA testing and performance optimization. Deployed the application to production.',
icon: 'i-lucide-check-circle',
value: 'deployment'
}] satisfies TimelineItem[]
</script>
<template>
<UTimeline :items="items" :default-value="2" class="w-96">
<template #development-title="{ item }">
<div class="flex items-center gap-1">
<span>{{ item.title }}</span>
<UAvatarGroup size="2xs">
<UAvatar v-for="(developer, index) of item.developers" :key="index" v-bind="developer" />
</UAvatarGroup>
</div>
</template>
</UTimeline>
</template>

View File

@@ -1,42 +0,0 @@
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'
const items: TimelineItem[] = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
icon: 'i-lucide-code',
value: 'development'
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
description: 'QA testing and performance optimization. Deployed the application to production.',
icon: 'i-lucide-check-circle',
value: 'deployment'
}]
const active = ref(0)
// Note: This is for demonstration purposes only. Don't do this at home.
onMounted(() => {
setInterval(() => {
active.value = (active.value + 1) % items.length
}, 2000)
})
</script>
<template>
<UTimeline v-model="active" :items="items" class="w-96" />
</template>

View File

@@ -1,60 +0,0 @@
<script lang="ts" setup>
import type { TimelineItem } from '@nuxt/ui'
import { useTimeAgo } from '@vueuse/core'
const items = [{
username: 'J-Michalek',
date: '2025-05-24T14:58:55Z',
action: 'opened this',
avatar: {
src: 'https://github.com/J-Michalek.png'
}
}, {
username: 'J-Michalek',
date: '2025-05-26T19:30:14+02:00',
action: 'marked this pull request as ready for review',
icon: 'i-lucide-check-circle'
}, {
username: 'benjamincanac',
date: '2025-05-27T11:01:20Z',
action: 'commented on this',
description: 'I\'ve made a few changes, let me know what you think! Basically I updated the design, removed unnecessary divs, used Avatar component for the indicator since it supports icon already.',
avatar: {
src: 'https://github.com/benjamincanac.png'
}
}, {
username: 'J-Michalek',
date: '2025-05-27T11:01:20Z',
action: 'commented on this',
description: 'Looks great! Good job on cleaning it up.',
avatar: {
src: 'https://github.com/J-Michalek.png'
}
}, {
username: 'benjamincanac',
date: '2025-05-27T11:01:20Z',
action: 'merged this',
icon: 'i-lucide-git-merge'
}] satisfies TimelineItem[]
</script>
<template>
<UTimeline
:items="items"
size="xs"
class="w-96"
:ui="{
date: 'float-end ms-1',
description: 'px-3 py-2 ring ring-default mt-2 rounded-md text-default'
}"
>
<template #title="{ item }">
<span>{{ item.username }}</span>
<span class="font-normal text-muted">&nbsp;{{ item.action }}</span>
</template>
<template #date="{ item }">
{{ useTimeAgo(new Date(item.date)) }}
</template>
</UTimeline>
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import { ShikiCachedRenderer } from 'shiki-stream/vue'
const colorMode = useColorMode()
const highlighter = await useHighlighter()
const props = defineProps<{
code: string
language: string
class?: string
meta?: string
}>()
const trimmedCode = computed(() => {
return props.code.trim().replace(/`+$/, '')
})
const lang = computed(() => {
switch (props.language) {
case 'vue':
return 'vue'
case 'javascript':
return 'js'
case 'typescript':
return 'ts'
case 'css':
return 'css'
default:
return props.language
}
})
const key = computed(() => {
return `${lang.value}-${colorMode.value}`
})
</script>
<template>
<ProsePre v-bind="props">
<ShikiCachedRenderer
:key="key"
:highlighter="highlighter"
:code="trimmedCode"
:lang="lang"
:theme="colorMode.value === 'dark' ? 'material-theme-palenight' : 'material-theme-lighter'"
/>
</ProsePre>
</template>

View File

@@ -0,0 +1,21 @@
import { createHighlighter, type HighlighterGeneric } from 'shiki'
import { createJavaScriptRegexEngine } from 'shiki/engine-javascript.mjs'
let highlighter: HighlighterGeneric<any, any> | null = null
let promise: Promise<HighlighterGeneric<any, any>> | null = null
export const useHighlighter = async () => {
if (!promise) {
promise = createHighlighter({
langs: ['vue', 'js', 'ts', 'css', 'html', 'json', 'yaml', 'markdown', 'bash'],
themes: ['material-theme-palenight', 'material-theme-lighter'],
engine: createJavaScriptRegexEngine()
})
}
if (!highlighter) {
highlighter = await promise
}
return highlighter
}

View File

@@ -1,66 +0,0 @@
export function useSearchLinks() {
return [{
label: 'Docs',
icon: 'i-lucide-square-play',
to: '/getting-started'
}, {
label: 'Components',
icon: 'i-lucide-square-code',
to: '/components'
}, {
icon: 'i-lucide-sparkles',
label: 'Pro > Features',
description: 'A collection of premium Vue components.',
to: '/pro'
}, {
icon: 'i-lucide-credit-card',
label: 'Pro > Pricing',
description: 'Free in development, buy when ready to launch.',
to: '/pro/pricing'
}, {
icon: 'i-lucide-panels-top-left',
label: 'Pro > Templates',
description: 'Official templates made with Nuxt UI Pro.',
to: '/pro/templates'
}, {
icon: 'i-lucide-circle-check',
label: 'Pro > Activate',
description: 'Enable Nuxt UI Pro in your production projects.',
to: '/pro/activate'
}, {
label: 'Figma',
icon: 'i-simple-icons-figma',
to: '/figma'
}, {
icon: 'i-lucide-presentation',
label: 'Community > Showcase',
description: 'Check out some of the amazing projects built with Nuxt UI.',
to: '/showcase'
}, {
label: 'Community > Contribution',
description: 'A comprehensive guide on contributing to Nuxt UI, including project structure, development workflow, and best practices.',
icon: 'i-lucide-git-pull-request-arrow',
to: '/getting-started/contribution'
}, {
label: 'Community > Roadmap',
description: 'Track our development progress in real-time.',
icon: 'i-lucide-map',
to: '/roadmap'
}, {
label: 'Community > Devtools',
description: 'Integrate Nuxt UI with Nuxt Devtools with Compodium.',
icon: 'i-lucide-code',
to: 'https://github.com/romhml/compodium',
target: '_blank'
}, {
label: 'Community > Team',
description: 'Meet the team behind Nuxt UI.',
icon: 'i-lucide-users',
to: '/team'
}, {
label: 'Releases',
icon: 'i-lucide-rocket',
to: 'https://github.com/nuxt/ui/releases',
target: '_blank'
}]
}

View File

@@ -15,7 +15,6 @@ const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSe
})
const links = useLinks()
const searchLinks = useSearchLinks()
const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white')
const radius = computed(() => `:root { --ui-radius: ${appConfig.theme.radius}rem; }`)
const blackAsPrimary = computed(() => appConfig.theme.blackAsPrimary ? `:root { --ui-primary: black; } .dark { --ui-primary: white; }` : ':root {}')
@@ -49,7 +48,6 @@ useServerSeoMeta({
useFaviconFromTheme()
const { frameworks, modules } = useSharedData()
const { mappedNavigation, filteredNavigation } = useContentNavigation(navigation)
provide('navigation', mappedNavigation)
@@ -67,22 +65,6 @@ provide('navigation', mappedNavigation)
<Footer />
<ClientOnly>
<LazyUContentSearch
:links="searchLinks"
:files="files"
:groups="[{
id: 'framework',
label: 'Framework',
items: frameworks
}, {
id: 'module',
label: 'Module',
items: modules
}]"
:navigation="filteredNavigation"
:fuse="{ resultLimit: 100 }"
/>
</ClientOnly>
<Search :files="files" :navigation="filteredNavigation" />
</UApp>
</template>

View File

@@ -225,7 +225,7 @@ pnpm run test:vue # for Vue
```
::tip
If you have to update the snapshots, press `u` after the tests have finished running.
If you have to update the snapshots, press `u` when running the tests.
::
### Commit Conventions

View File

@@ -62,7 +62,7 @@ Update an overlay using its `id`
- `id`: The identifier of the overlay
- `props`: An object of props to update on the rendered component.
### `unmount(id: symbol): void`
### `unMount(id: symbol): void`
Removes the overlay from the DOM using its `id`

View File

@@ -258,13 +258,13 @@ This also works with the [Form](/components/form) component.
### 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-refresh-cw`.
::component-code
---
props:
loading: true
loadingIcon: 'i-lucide-loader'
loadingIcon: 'i-lucide-repeat-2'
slots:
default: Button
---

View File

@@ -279,7 +279,7 @@ props:
### 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-refresh-cw`.
::component-code
---
@@ -295,7 +295,7 @@ class: '!p-0'
props:
autofocus: false
loading: true
loadingIcon: 'i-lucide-loader'
loadingIcon: 'i-lucide-repeat-2'
groups:
- id: 'apps'
items:

View File

@@ -291,7 +291,7 @@ In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts),
This allows you to move the trigger outside of the Drawer or remove it entirely.
::
### Disable dismissal
### Prevent closing
Set the `dismissible` prop to `false` to prevent the Drawer from being closed when clicking outside of it or pressing escape.
@@ -306,17 +306,6 @@ name: 'drawer-dismissible-example'
In this example, the `header` slot is used to add a close button which is not done by default.
::
### With interactive background
Set the `overlay` and `modal` props to `false` alongside the `dismissible` prop to make the Drawer's background interactive without closing the Drawer.
::component-example
---
prettier: true
name: 'drawer-modal-example'
---
::
### Responsive drawer
You can render a [Modal](/components/modal) component on desktop and a Drawer on mobile for example.

View File

@@ -518,7 +518,7 @@ props:
### 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-refresh-cw`.
::component-code
---
@@ -532,7 +532,7 @@ external:
props:
modelValue: 'Backlog'
loading: true
loadingIcon: 'i-lucide-loader'
loadingIcon: 'i-lucide-repeat-2'
items:
- Backlog
- Todo
@@ -612,7 +612,7 @@ props:
---
::
### With icon in items
### With icons in items
You can use the `icon` property to display an [Icon](/components/icon) inside the items.

View File

@@ -172,7 +172,7 @@ props:
### 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-refresh-cw`.
::component-code
---
@@ -180,7 +180,7 @@ ignore:
- placeholder
props:
loading: true
loadingIcon: 'i-lucide-loader'
loadingIcon: 'i-lucide-repeat-2'
placeholder: 'Search...'
---
::

View File

@@ -274,7 +274,7 @@ In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts),
This allows you to move the trigger outside of the Modal or remove it entirely.
::
### Disable dismissal
### Prevent closing
Set the `dismissible` prop to `false` to prevent the Modal from being closed when clicking outside of it or pressing escape. A `close:prevent` event will be emitted when the user tries to close it.
@@ -305,13 +305,13 @@ slots:
### Programmatic usage
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Modal programmatically.
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Modal programatically.
::warning
Make sure to wrap your app with the [`App`](/components/app) component which uses the [`OverlayProvider`](https://github.com/nuxt/ui/blob/v3/src/runtime/components/OverlayProvider.vue) component.
::
First, create a modal component that will be opened programmatically:
First, create a modal component that will be opened programatically:
::component-example
---

View File

@@ -23,7 +23,8 @@ Use the `items` prop as an array of objects with the following properties:
- `badge?: string | number | BadgeProps`{lang="ts-type"}
- `tooltip?: TooltipProps`{lang="ts-type"}
- `trailingIcon?: string`{lang="ts-type"}
- `type?: 'label' | 'trigger' | 'link'`{lang="ts-type"}
- `type?: 'label' | 'link'`{lang="ts-type"}
- `collapsible?: boolean`{lang="ts-type"}
- `defaultOpen?: boolean`{lang="ts-type"}
- `open?: boolean`{lang="ts-type"}
- `value?: string`{lang="ts-type"}
@@ -32,7 +33,7 @@ Use the `items` prop as an array of objects with the following properties:
- `onSelect?(e: Event): void`{lang="ts-type"}
- `children?: NavigationMenuChildItem[]`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { linkLeadingAvatarSize?: ClassNameValue, linkLeadingAvatar?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingBadgeSize?: ClassNameValue, linkTrailingBadge?: ClassNameValue, linkTrailingIcon?: ClassNameValue, label?: ClassNameValue, link?: ClassNameValue, content?: ClassNameValue, childList?: ClassNameValue, childLabel?: ClassNameValue, childItem?: ClassNameValue, childLink?: ClassNameValue, childLinkIcon?: ClassNameValue, childLinkWrapper?: ClassNameValue, childLinkLabel?: ClassNameValue, childLinkLabelExternalIcon?: ClassNameValue, childLinkDescription?: ClassNameValue }`{lang="ts-type"}
- `ui?: { linkLeadingAvatarSize?: ClassNameValue, linkLeadingAvatar?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingBadgeSize?: ClassNameValue, linkTrailingBadge?: ClassNameValue, linkTrailingIcon?: ClassNameValue, label?: ClassNameValue, link?: ClassNameValue, content?: ClassNameValue, childList?: ClassNameValue, childItem?: ClassNameValue, childLink?: ClassNameValue, childLinkIcon?: ClassNameValue, childLinkWrapper?: ClassNameValue, childLinkLabel?: ClassNameValue, childLinkLabelExternalIcon?: ClassNameValue, childLinkDescription?: ClassNameValue }`{lang="ts-type"}
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
@@ -144,7 +145,7 @@ Each item can take a `children` array of objects with the following properties t
Use the `orientation` prop to change the orientation of the NavigationMenu.
::note
When orientation is `vertical`, an [Accordion](/components/accordion) component is used to display each group. You can control the open state of each item using the `open` and `defaultOpen` properties and change the behavior using the [`collapsible`](/components/accordion#collapsible) and [`type`](/components/accordion#multiple) props.
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children. You can control the open state of each item using the `open` and `defaultOpen` properties.
::
::component-code
@@ -243,11 +244,7 @@ Groups will be spaced when orientation is `horizontal` and separated when orient
### Collapsed
In `vertical` orientation, use the `collapsed` prop to collapse the NavigationMenu, this can be useful in a sidebar for example.
::note
You can use the [`tooltip`](#with-tooltip-in-items) and [`popover`](#with-popover-in-items) props to display more information on the collapsed items.
::
Use the `collapsed` prop to collapse the NavigationMenu, this can be useful in a sidebar for example.
::component-code
---
@@ -260,17 +257,8 @@ external:
- items
externalTypes:
- NavigationMenuItem[][]
items:
tooltip:
- true
- false
popover:
- true
- false
props:
collapsed: true
tooltip: false
popover: false
orientation: 'vertical'
items:
- - label: Links
@@ -295,6 +283,8 @@ props:
description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
- label: Composables
icon: i-lucide-database
collapsible: false
open: false
children:
- label: defineShortcuts
icon: i-lucide-file-text
@@ -310,6 +300,8 @@ props:
to: /composables/use-toast
- label: Components
icon: i-lucide-box
collapsible: false
open: false
to: /components
active: true
children:
@@ -348,6 +340,10 @@ props:
---
::
::tip
You can set the `collapsible: false` property on items with children to prevent them from being collapsible. This allows the item to act as a regular link while still displaying its children in a submenu.
::
### Highlight
Use the `highlight` prop to display a highlighted border for the active item.
@@ -889,11 +885,9 @@ You can inspect the DOM to see each item's content being rendered.
## Examples
### With tooltip in items :badge{label="New" class="align-text-top"}
### With tooltips in items :badge{label="New" class="align-text-top"}
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.
You can pass any property from the [Tooltip](/components/tooltip) component globally or on each item.
You can use the `tooltip` property to display a [Tooltip](/components/tooltip) around an item. This can be useful when the menu is collapsed.
::component-code
---
@@ -906,12 +900,7 @@ external:
- items
externalTypes:
- NavigationMenuItem[][]
items:
tooltip:
- true
- false
props:
tooltip: true
collapsed: true
orientation: 'vertical'
items:
@@ -919,24 +908,40 @@ props:
type: 'label'
- label: Guide
icon: i-lucide-book-open
tooltip:
text: 'Guide'
children:
- label: Introduction
description: Fully styled and customizable components for Nuxt.
icon: i-lucide-house
tooltip:
text: 'Introduction'
- label: Installation
description: Learn how to install and configure Nuxt UI in your application.
icon: i-lucide-cloud-download
tooltip:
text: 'Installation'
- label: 'Icons'
icon: 'i-lucide-smile'
description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
tooltip:
text: 'Icons'
- label: 'Colors'
icon: 'i-lucide-swatch-book'
description: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
tooltip:
text: 'Colors'
- label: 'Theme'
icon: 'i-lucide-cog'
description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
tooltip:
text: 'Theme'
- label: Composables
icon: i-lucide-database
tooltip:
text: 'Composables'
collapsible: false
open: false
children:
- label: defineShortcuts
icon: i-lucide-file-text
@@ -952,8 +957,12 @@ props:
to: /composables/use-toast
- label: Components
icon: i-lucide-box
tooltip:
text: 'Components'
to: /components
active: true
collapsible: false
open: false
children:
- label: Link
icon: i-lucide-file-text
@@ -985,126 +994,17 @@ props:
to: https://github.com/nuxt/ui
target: _blank
tooltip:
text: 'Open on GitHub'
text: 'GitHub'
kbds:
- 3.8k
- label: Help
icon: i-lucide-circle-help
disabled: true
---
::
### With popover in items :badge{label="New" class="align-text-top"}
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.
You can pass any property from the [Popover](/components/popover) component globally or on each item.
::component-code
---
collapse: true
ignore:
- items
- orientation
- class
external:
- items
externalTypes:
- NavigationMenuItem[][]
items:
popover:
- true
- false
props:
popover: true
collapsed: true
orientation: 'vertical'
items:
- - label: Links
type: 'label'
- label: Guide
icon: i-lucide-book-open
children:
- label: Introduction
description: Fully styled and customizable components for Nuxt.
icon: i-lucide-house
- label: Installation
description: Learn how to install and configure Nuxt UI in your application.
icon: i-lucide-cloud-download
- label: 'Icons'
icon: 'i-lucide-smile'
description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
- label: 'Colors'
icon: 'i-lucide-swatch-book'
description: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
- label: 'Theme'
icon: 'i-lucide-cog'
description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
- label: Composables
icon: i-lucide-database
popover:
mode: 'click'
children:
- label: defineShortcuts
icon: i-lucide-file-text
description: Define shortcuts for your application.
to: /composables/define-shortcuts
- label: useOverlay
icon: i-lucide-file-text
description: Display a modal/slideover within your application.
to: /composables/use-overlay
- label: useToast
icon: i-lucide-file-text
description: Display a toast within your application.
to: /composables/use-toast
- label: Components
icon: i-lucide-box
to: /components
active: true
children:
- label: Link
icon: i-lucide-file-text
description: Use NuxtLink with superpowers.
to: /components/link
- label: Modal
icon: i-lucide-file-text
description: Display a modal within your application.
to: /components/modal
- label: NavigationMenu
icon: i-lucide-file-text
description: Display a list of links.
to: /components/navigation-menu
- label: Pagination
icon: i-lucide-file-text
description: Display a list of pages.
to: /components/pagination
- label: Popover
icon: i-lucide-file-text
description: Display a non-modal dialog that floats around a trigger element.
to: /components/popover
- label: Progress
icon: i-lucide-file-text
description: Show a horizontal bar to indicate task progression.
to: /components/progress
- - label: GitHub
icon: i-simple-icons-github
badge: 3.8k
to: https://github.com/nuxt/ui
target: _blank
tooltip:
text: 'Open on GitHub'
kbds:
- 3.8k
- label: Help
icon: i-lucide-circle-help
disabled: true
text: 'Help'
---
::
::tip{to="#with-content-slot"}
You can use the `#content` slot to customize the content of the popover in the `vertical` orientation.
::
### Control active item
You can control the active item by using the `default-value` prop or the `v-model` directive with the index of the item.
@@ -1152,7 +1052,6 @@ Use the `#item-content` slot or the `slot` property (`#{{ item.slot }}-content`)
::component-example
---
collapse: true
name: 'navigation-menu-content-slot-example'
---
::

View File

@@ -181,7 +181,7 @@ name: 'popover-open-example'
In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts), you can toggle the Popover by pressing :kbd{value="O"}.
::
### Disable dismissal
### Prevent closing
Set the `dismissible` prop to `false` to prevent the Popover from being closed when clicking outside of it or pressing escape. A `close:prevent` event will be emitted when the user tries to close it.
@@ -202,21 +202,6 @@ name: 'popover-command-palette-example'
---
::
### With anchor slot :badge{label="New" class="align-text-top"}
You can use the `#anchor` slot to position the Popover against a custom element.
::warning
This slot only works when `mode` is `click`.
::
::component-example
---
collapse: true
name: 'popover-anchor-slot-example'
---
::
## API
### Props

View File

@@ -555,7 +555,7 @@ props:
### 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-refresh-cw`.
::component-code
---
@@ -570,7 +570,7 @@ external:
props:
modelValue: 'Backlog'
loading: true
loadingIcon: 'i-lucide-loader'
loadingIcon: 'i-lucide-repeat-2'
items:
- Backlog
- Todo
@@ -655,7 +655,7 @@ props:
---
::
### With icon in items
### With icons in items
You can use the `icon` property to display an [Icon](/components/icon) inside the items.

View File

@@ -507,7 +507,7 @@ props:
### 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-refresh-cw`.
::component-code
---
@@ -522,7 +522,7 @@ external:
props:
modelValue: 'Backlog'
loading: true
loadingIcon: 'i-lucide-loader'
loadingIcon: 'i-lucide-repeat-2'
items:
- Backlog
- Todo
@@ -607,7 +607,7 @@ props:
---
::
### With icon in items
### With icons in items
You can use the `icon` property to display an [Icon](/components/icon) inside the items.

View File

@@ -273,7 +273,7 @@ In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts),
This allows you to move the trigger outside of the Slideover or remove it entirely.
::
### Disable dismissal
### Prevent closing
Set the `dismissible` prop to `false` to prevent the Slideover from being closed when clicking outside of it or pressing escape. A `close:prevent` event will be emitted when the user tries to close it.
@@ -304,13 +304,13 @@ slots:
### Programmatic usage
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Slideover programmatically.
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Slideover programatically.
::warning
Make sure to wrap your app with the [`App`](/components/app) component which uses the [`OverlayProvider`](https://github.com/nuxt/ui/blob/v3/src/runtime/components/OverlayProvider.vue) component.
::
First, create a slideover component that will be opened programmatically:
First, create a slideover component that will be opened programatically:
::component-example
---

View File

@@ -200,10 +200,6 @@ Use the `#content` slot to customize the content of each item.
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}`{lang="ts-type"}
:component-example{name="stepper-custom-slot-example"}
## API

View File

@@ -109,7 +109,7 @@ props:
### 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-refresh-cw`.
::component-code
---
@@ -118,7 +118,7 @@ ignore:
- defaultValue
props:
loading: true
loadingIcon: 'i-lucide-loader'
loadingIcon: 'i-lucide-repeat-2'
defaultValue: true
label: Check me
---

View File

@@ -222,10 +222,6 @@ Use the `#content` slot to customize the content of each item.
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}`{lang="ts-type"}
:component-example{name="tabs-custom-slot-example"}
## API

View File

@@ -194,7 +194,7 @@ props:
### Loading Icon :badge{label="New" class="align-text-top"}
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-refresh-cw`.
::component-code
---
@@ -202,7 +202,7 @@ ignore:
- placeholder
props:
loading: true
loadingIcon: 'i-lucide-loader'
loadingIcon: 'i-lucide-repeat-2'
placeholder: 'Search...'
rows: 1
---

View File

@@ -1,228 +0,0 @@
---
title: Timeline
description: 'A component that displays a sequence of events with dates, titles, icons or avatars.'
category: data
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Timeline.vue
navigation.badge: Soon
---
## Usage
### Items
Use the `items` prop as an array of objects with the following properties:
- `date?: string`{lang="ts-type"}
- `title?: string`{lang="ts-type"}
- `description?: AvatarProps`{lang="ts-type"}
- `icon?: string`{lang="ts-type"}
- `avatar?: AvatarProps`{lang="ts-type"}
- `value?: string | number`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, indicator?: ClassNameValue, separator?: ClassNameValue, wrapper?: ClassNameValue, separator?: ClassNameValue, date?: ClassNameValue, title?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"}
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops. Created wireframes and prototypes for user testing.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development. Implemented core features and integrated with APIs.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization. Deployed the application to production.'
icon: 'i-lucide-check-circle'
class: 'w-96'
---
::
### Color
Use the `color` prop to change the color of the active items in a Timeline.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
color: neutral
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops. Created wireframes and prototypes for user testing.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development. Implemented core features and integrated with APIs.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization. Deployed the application to production.'
icon: 'i-lucide-check-circle'
class: 'w-96'
---
::
### Size
Use the `size` prop to change the size of the Timeline.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
size: xs
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops. Created wireframes and prototypes for user testing.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development. Implemented core features and integrated with APIs.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization. Deployed the application to production.'
icon: 'i-lucide-check-circle'
class: 'w-96'
---
::
### Orientation
Use the `orientation` prop to change the orientation of the Timeline. Defaults to `vertical`.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
orientation: 'horizontal'
defaultValue: 2
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'
---
::
## Examples
### Control active item
You can control the active item by using the `default-value` prop or the `v-model` directive with the index of the item.
:component-example{name="timeline-model-value-example" prettier}
::tip
You can also pass the `value` of one of the items if provided.
::
### With alternating layout
Use the `ui` prop to create a Timeline with alternating layout.
:component-example{name="timeline-alternating-layout-example" prettier}
### With custom slot
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}-indicator`{lang="ts-type"}
- `#{{ item.slot }}-date`{lang="ts-type"}
- `#{{ item.slot }}-title`{lang="ts-type"}
- `#{{ item.slot }}-description`{lang="ts-type"}
:component-example{name="timeline-custom-slot-example" prettier}
### With slots
Use the available slots to create a more complex Timeline.
:component-example{name="timeline-slots-example" prettier}
## API
### Props
:component-props
### Slots
:component-slots
### Emits
:component-emits
## Theme
:component-theme

View File

@@ -407,14 +407,7 @@ This lets you select a parent item without expanding or collapsing its children.
### With custom slot
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}`{lang="ts-type"}
- `#{{ item.slot }}-leading`{lang="ts-type"}
- `#{{ item.slot }}-label`{lang="ts-type"}
- `#{{ item.slot }}-trailing`{lang="ts-type"}
Use the `item.slot` property to customize a specific item.
::component-example
---

View File

@@ -5,38 +5,39 @@
"dependencies": {
"@ai-sdk/vue": "^1.2.12",
"@iconify-json/logos": "^1.2.4",
"@iconify-json/lucide": "^1.2.44",
"@iconify-json/simple-icons": "^1.2.35",
"@iconify-json/lucide": "^1.2.43",
"@iconify-json/simple-icons": "^1.2.34",
"@iconify-json/vscode-icons": "^1.2.21",
"@nuxt/content": "^3.5.1",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@f06b49c",
"@nuxt/ui-pro": "^3.1.2",
"@nuxthub/core": "^0.8.27",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^22.0.0",
"@octokit/rest": "^21.1.1",
"@rollup/plugin-yaml": "^4.1.2",
"@vueuse/integrations": "^13.2.0",
"@vueuse/nuxt": "^13.2.0",
"ai": "^4.3.16",
"ai": "^4.3.15",
"capture-website": "^4.2.0",
"joi": "^17.13.3",
"motion-v": "^1.1.1",
"nuxt": "^3.17.4",
"motion-v": "^1.0.2",
"nuxt": "^3.17.3",
"nuxt-component-meta": "^0.11.0",
"nuxt-llms": "^0.1.2",
"nuxt-og-image": "^5.1.4",
"nuxt-og-image": "^5.1.3",
"prettier": "^3.5.3",
"shiki-stream": "^0.1.2",
"shiki-transformer-color-highlight": "^1.0.0",
"sortablejs": "^1.15.6",
"superstruct": "^2.0.2",
"ufo": "^1.6.1",
"valibot": "^1.1.0",
"workers-ai-provider": "^0.5.2",
"workers-ai-provider": "^0.4.1",
"yup": "^1.6.1",
"zod": "^3.25.28"
"zod": "^3.24.4"
},
"devDependencies": {
"wrangler": "^4.16.1"
"wrangler": "^4.15.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,5 +1,6 @@
import { streamText } from 'ai'
import { streamText, tool } from 'ai'
import { createWorkersAI } from 'workers-ai-provider'
import { z } from 'zod'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
@@ -12,11 +13,30 @@ export default defineEventHandler(async (event) => {
}
: undefined
const workersAI = createWorkersAI({ binding: hubAI(), gateway })
const autorag = hubAutoRAG('ui3')
return streamText({
model: workersAI('@cf/meta/llama-3.2-3b-instruct'),
maxTokens: 10000,
system: 'You are a helpful assistant that can answer questions and help.',
messages
model: workersAI('@cf/meta/llama-3.3-70b-instruct-fp8-fast'),
messages,
system: `You are a helpful assistant for Nuxt UI. Check your knowledge base before answering any questions.
Only respond to questions using information from tool calls.
if no relevant information is found in the tool calls, respond, "Sorry, I don't know."
Format your markdown response using the following rules:
- Use the vue lang for code blocks syntax highlighting.
- Don't use markdown headings.
`,
tools: {
searchDocumentation: tool({
description: `search the documentation for information to answer questions.`,
parameters: z.object({
question: z.string().describe('the users question')
}),
execute: async ({ question }) => {
return (await autorag.aiSearch({
query: question
})).response
}
})
}
}).toDataStreamResponse()
})

View File

@@ -1,7 +1,7 @@
{
"name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "3.1.3",
"version": "3.1.2",
"packageManager": "pnpm@10.11.0",
"repository": {
"type": "git",
@@ -112,19 +112,19 @@
"release": "release-it"
},
"dependencies": {
"@iconify/vue": "^5.0.0",
"@internationalized/date": "^3.8.1",
"@internationalized/number": "^3.6.2",
"@iconify/vue": "^4.3.0",
"@internationalized/date": "^3.8.0",
"@internationalized/number": "^3.6.1",
"@nuxt/fonts": "^0.11.4",
"@nuxt/icon": "^1.13.0",
"@nuxt/kit": "^3.17.4",
"@nuxt/schema": "^3.17.4",
"@nuxt/icon": "^1.12.0",
"@nuxt/kit": "^3.17.3",
"@nuxt/schema": "^3.17.3",
"@nuxtjs/color-mode": "^3.5.2",
"@standard-schema/spec": "^1.0.0",
"@tailwindcss/postcss": "^4.1.7",
"@tailwindcss/vite": "^4.1.7",
"@tailwindcss/postcss": "^4.1.6",
"@tailwindcss/vite": "^4.1.6",
"@tanstack/vue-table": "^8.21.3",
"@unhead/vue": "^2.0.10",
"@unhead/vue": "^2.0.8",
"@vueuse/core": "^13.2.0",
"@vueuse/integrations": "^13.2.0",
"colortranslator": "^4.1.0",
@@ -144,29 +144,29 @@
"mlly": "^1.7.4",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"reka-ui": "^2.3.0",
"reka-ui": "^2.2.1",
"scule": "^1.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.7",
"tinyglobby": "^0.2.14",
"tailwindcss": "^4.1.6",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.4",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.7.0",
"unplugin-auto-import": "^19.2.0",
"unplugin-vue-components": "^28.5.0",
"vaul-vue": "^0.4.1",
"vue-component-type-helpers": "^2.2.10"
},
"devDependencies": {
"@nuxt/eslint-config": "^1.4.1",
"@nuxt/eslint-config": "^1.3.1",
"@nuxt/module-builder": "^1.0.1",
"@nuxt/test-utils": "^3.19.1",
"@nuxt/test-utils": "^3.18.0",
"@release-it/conventional-changelog": "^10.0.1",
"@vue/test-utils": "^2.4.6",
"embla-carousel": "^8.6.0",
"eslint": "^9.27.0",
"eslint": "^9.26.0",
"happy-dom": "^17.4.7",
"nuxt": "^3.17.4",
"nuxt": "^3.17.3",
"release-it": "^19.0.2",
"vitest": "^3.1.4",
"vitest": "^3.1.3",
"vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.2.10"
},

View File

@@ -11,9 +11,9 @@
},
"dependencies": {
"@nuxt/ui": "latest",
"vue": "^3.5.15",
"vue": "^3.5.14",
"vue-router": "^4.5.1",
"zod": "^3.25.28"
"zod": "^3.24.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.4",

View File

@@ -61,7 +61,6 @@ const components = [
'tabs',
'table',
'textarea',
'timeline',
'toast',
'tooltip',
'tree'

View File

@@ -61,7 +61,6 @@ const components = [
'tabs',
'table',
'textarea',
'timeline',
'toast',
'tooltip',
'tree'

View File

@@ -2,23 +2,19 @@
import { CalendarDate } from '@internationalized/date'
const singleValue = shallowRef(new CalendarDate(2022, 1, 10))
const multipleValue = shallowRef([new CalendarDate(2022, 1, 10), new CalendarDate(2022, 1, 20)])
const rangeValue = shallowRef({
const multipleValue = shallowRef({
start: new CalendarDate(2022, 1, 10),
end: new CalendarDate(2022, 1, 20)
})
</script>
<template>
<div class="flex gap-4">
<div class="flex flex-col gap-4">
<div class="flex justify-center gap-2">
<UCalendar v-model="singleValue" />
</div>
<div class="flex justify-center gap-2">
<UCalendar v-model="multipleValue" multiple />
</div>
<div class="flex justify-center gap-2">
<UCalendar v-model="rangeValue" range />
<UCalendar v-model="multipleValue" range />
</div>
</div>
</template>

View File

@@ -47,7 +47,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 w-48">
<UInputMenu :items="items" autofocus placeholder="Search..." default-value="Apple" />
<UInputMenu :items="items" autofocus placeholder="Search..." />
</div>
<div class="flex items-center gap-2">
<UInputMenu

View File

@@ -69,13 +69,5 @@ function openModal() {
</UModal>
<UButton label="Open programmatically" color="neutral" variant="outline" @click="openModal" />
<UModal title="First modal">
<UButton color="neutral" variant="outline" label="Close with scoped slot close" />
<template #footer="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
</UModal>
</div>
</template>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'
import theme from '#build/ui/navigation-menu'
const colors = Object.keys(theme.variants.color)
@@ -14,9 +13,6 @@ const orientation = ref('horizontal' as const)
const contentOrientation = ref('horizontal' as const)
const highlight = ref(true)
const collapsed = ref(false)
const tooltip = ref(false)
const popover = ref(false)
const arrow = ref(false)
const items = [
[{
@@ -46,8 +42,7 @@ const items = [
}, {
label: 'Components',
icon: 'i-lucide-box',
to: '/components/navigation-menu',
type: 'trigger',
to: '/components',
active: true,
defaultOpen: true,
children: [{
@@ -92,7 +87,7 @@ const items = [
icon: 'i-lucide-circle-help',
disabled: true
}]
] satisfies NavigationMenuItem[][]
]
</script>
<template>
@@ -105,15 +100,10 @@ const items = [
<USwitch v-model="collapsed" label="Collapsed" />
<USwitch v-model="highlight" label="Highlight" />
<USelect v-model="highlightColor" :items="colors" placeholder="Highlight color" />
<USwitch v-model="tooltip" label="Tooltip" />
<USwitch v-model="popover" label="Popover" />
<USwitch v-model="arrow" label="Arrow" />
</div>
<UNavigationMenu
:arrow="arrow"
:tooltip="tooltip"
:popover="popover"
arrow
:collapsed="collapsed"
:items="items"
:color="color"

View File

@@ -5,7 +5,7 @@ const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.varia
const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme.variants.variant>
const onComplete = (e: string[]) => {
console.log(e)
alert(e.join(''))
}
</script>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
const open = ref(false)
const openCustomAnchor = ref(false)
const loading = ref(false)
function send() {
@@ -52,21 +51,6 @@ function send() {
</div>
</template>
</UPopover>
<div class="mt-8 relative">
<UPopover
v-model:open="openCustomAnchor"
:dismissible="false"
>
<template #anchor>
<UInput placeholder="Search" class="w-56" @focus="openCustomAnchor = true" />
</template>
<template #content>
<Placeholder class="size-48 m-4 inline-flex" />
</template>
</UPopover>
</div>
</div>
<div class="mt-24">

View File

@@ -52,7 +52,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 w-48">
<USelectMenu :items="items" placeholder="Search..." default-value="Apple" />
<USelectMenu :items="items" placeholder="Search..." />
</div>
<div class="flex items-center gap-2">
<USelectMenu

View File

@@ -125,21 +125,5 @@ function openSlideover() {
</USlideover>
<UButton label="Open programmatically" color="neutral" variant="outline" @click="openSlideover" />
<USlideover title="Slideover with scoped slot close" description="This slideover has a scoped slot close that can be used to close the slideover from within the content.">
<UButton color="neutral" variant="subtle" label="Open with scoped slot close" />
<template #header="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
<template #body="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
<template #footer="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
</USlideover>
</div>
</template>

View File

@@ -1,60 +0,0 @@
<script lang="ts" setup>
import type { TimelineItem } from '@nuxt/ui'
import theme from '#build/ui/timeline'
const sizes = Object.keys(theme.variants.size)
const colors = Object.keys(theme.variants.color)
const orientations = Object.keys(theme.variants.orientation)
const orientation = ref('vertical' as const)
const color = ref('primary' as const)
const size = ref('md' as const)
const items = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
description: 'User research and design workshops. Created wireframes and prototypes for user testing',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
icon: 'i-lucide-code',
value: 'development'
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
description: 'QA testing and performance optimization. Deployed the application to production.',
icon: 'i-lucide-check-circle',
value: 'deployment'
}] satisfies TimelineItem[]
const value = ref('kickoff')
</script>
<template>
<div class="flex flex-col gap-10">
<div class="flex items-center justify-center gap-2">
<USelect v-model="color" :items="colors" placeholder="Color" />
<USelect v-model="orientation" :items="orientations" placeholder="Orientation" />
<USelect v-model="size" :items="sizes" placeholder="Size" />
<USelect v-model="value" :items="items.map(item => item.value)" placeholder="Value" />
</div>
<UTimeline
v-model="value"
:color="color"
:orientation="orientation"
:size="size"
:items="items"
class="data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-96"
/>
</div>
</template>

View File

@@ -9,12 +9,12 @@
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.44",
"@iconify-json/simple-icons": "^1.2.35",
"@iconify-json/lucide": "^1.2.43",
"@iconify-json/simple-icons": "^1.2.34",
"@nuxt/ui": "latest",
"@nuxthub/core": "^0.8.27",
"nuxt": "^3.17.4",
"zod": "^3.25.28"
"nuxt": "^3.17.3",
"zod": "^3.24.4"
},
"devDependencies": {
"typescript": "^5.8.3",

3772
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/avatar'
import type { ChipProps } from '../types'
import type { ComponentConfig } from '../types/utils'
type Avatar = ComponentConfig<typeof theme, AppConfig, 'avatar'>
@@ -23,7 +22,6 @@ export interface AvatarProps {
* @defaultValue 'md'
*/
size?: Avatar['variants']['size']
chip?: boolean | ChipProps
class?: any
style?: any
ui?: Avatar['slots']
@@ -42,7 +40,6 @@ import ImageComponent from '#build/ui-image-component'
import { useAvatarGroup } from '../composables/useAvatarGroup'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
import UChip from './Chip.vue'
defineOptions({ inheritAttrs: false })
@@ -84,13 +81,7 @@ function onError() {
</script>
<template>
<component
:is="props.chip ? UChip : Primitive"
:as="as"
v-bind="props.chip ? (typeof props.chip === 'object' ? { inset: true, ...props.chip } : { inset: true }) : {}"
:class="ui.root({ class: [props.ui?.root, props.class] })"
:style="props.style"
>
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })" :style="props.style">
<component
:is="ImageComponent"
v-if="src && !error"
@@ -110,5 +101,5 @@ function onError() {
<span v-else :class="ui.fallback({ class: props.ui?.fallback })">{{ fallback || '&nbsp;' }}</span>
</slot>
</Slot>
</component>
</Primitive>
</template>

View File

@@ -153,8 +153,8 @@ const Calendar = computed(() => props.range ? RangeCalendar : SingleCalendar)
<Calendar.Root
v-slot="{ weekDays, grid }"
v-bind="rootProps"
:model-value="(modelValue as DateValue | DateValue[])"
:default-value="(defaultValue as DateValue)"
:model-value="modelValue"
:default-value="defaultValue"
:locale="locale"
:dir="dir"
:class="ui.root({ class: [props.ui?.root, props.class] })"

View File

@@ -336,7 +336,6 @@ defineExpose({
<button
:aria-label="t('carousel.goto', { slide: index + 1 })"
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
:data-state="selectedIndex === index ? 'active' : undefined"
@click="scrollTo(index)"
/>
</template>

View File

@@ -280,7 +280,7 @@ const groups = computed(() => {
</ListboxFilter>
<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="groups?.length" :class="ui.viewport({ class: props.ui?.viewport })">
<ListboxGroup v-for="group in groups" :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 })">
{{ get(group, props.labelKey as string) }}

View File

@@ -109,70 +109,68 @@ const groups = computed<ContextMenuItem[][]>(() =>
<component :is="sub ? ContextMenu.SubContent : ContextMenu.Content" :class="props.class" v-bind="contentProps">
<slot name="content-top" />
<div role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<ContextMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ContextMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
<ReuseItemTemplate :item="item" :index="index" />
</ContextMenu.Label>
<ContextMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
<ContextMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
<ContextMenu.SubTrigger
as="button"
type="button"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
>
<ReuseItemTemplate :item="item" :index="index" />
</ContextMenu.SubTrigger>
<UContextMenuContent
sub
:class="props.class"
:ui="ui"
:ui-override="uiOverride"
:portal="portal"
:items="(item.children as T)"
:align-offset="-4"
:label-key="labelKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
v-bind="item.content"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData">
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
</template>
</UContextMenuContent>
</ContextMenu.Sub>
<ContextMenu.CheckboxItem
v-else-if="item.type === 'checkbox'"
:model-value="item.checked"
<ContextMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ContextMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
<ReuseItemTemplate :item="item" :index="index" />
</ContextMenu.Label>
<ContextMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
<ContextMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
<ContextMenu.SubTrigger
as="button"
type="button"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
@update:model-value="item.onUpdateChecked"
@select="item.onSelect"
>
<ReuseItemTemplate :item="item" :index="index" />
</ContextMenu.CheckboxItem>
<ContextMenu.Item
v-else
as-child
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
@select="item.onSelect"
</ContextMenu.SubTrigger>
<UContextMenuContent
sub
:class="props.class"
:ui="ui"
:ui-override="uiOverride"
:portal="portal"
:items="(item.children as T)"
:align-offset="-4"
:label-key="labelKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
v-bind="item.content"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], active, color: item?.color })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />
</ULinkBase>
</ULink>
</ContextMenu.Item>
</template>
</ContextMenu.Group>
</div>
<template v-for="(_, name) in proxySlots" #[name]="slotData">
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
</template>
</UContextMenuContent>
</ContextMenu.Sub>
<ContextMenu.CheckboxItem
v-else-if="item.type === 'checkbox'"
:model-value="item.checked"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
@update:model-value="item.onUpdateChecked"
@select="item.onSelect"
>
<ReuseItemTemplate :item="item" :index="index" />
</ContextMenu.CheckboxItem>
<ContextMenu.Item
v-else
as-child
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
@select="item.onSelect"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], active, color: item?.color })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />
</ULinkBase>
</ULink>
</ContextMenu.Item>
</template>
</ContextMenu.Group>
<slot />

View File

@@ -56,7 +56,7 @@ export interface DrawerSlots {
<script setup lang="ts">
import { computed, toRef } from 'vue'
import { VisuallyHidden, useForwardPropsEmits } from 'reka-ui'
import { useForwardPropsEmits } from 'reka-ui'
import { DrawerRoot, DrawerTrigger, DrawerPortal, DrawerOverlay, DrawerContent, DrawerTitle, DrawerDescription, DrawerHandle } from 'vaul-vue'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -101,20 +101,6 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.drawer || {}
<DrawerContent :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-bind="contentProps" v-on="contentEvents">
<DrawerHandle v-if="handle" :class="ui.handle({ class: props.ui?.handle })" />
<VisuallyHidden v-if="!!slots.content && ((title || !!slots.title) || (description || !!slots.description))">
<DrawerTitle v-if="title || !!slots.title">
<slot name="title">
{{ title }}
</slot>
</DrawerTitle>
<DrawerDescription v-if="description || !!slots.description">
<slot name="description">
{{ description }}
</slot>
</DrawerDescription>
</VisuallyHidden>
<slot name="content">
<div :class="ui.container({ class: props.ui?.container })">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description)" :class="ui.header({ class: props.ui?.header })">

View File

@@ -115,72 +115,70 @@ const groups = computed<DropdownMenuItem[][]>(() =>
<component :is="sub ? DropdownMenu.SubContent : DropdownMenu.Content" :class="props.class" v-bind="contentProps">
<slot name="content-top" />
<div role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<DropdownMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<DropdownMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
<ReuseItemTemplate :item="item" :index="index" />
</DropdownMenu.Label>
<DropdownMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
<DropdownMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
<DropdownMenu.SubTrigger
as="button"
type="button"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
>
<ReuseItemTemplate :item="item" :index="index" />
</DropdownMenu.SubTrigger>
<UDropdownMenuContent
sub
:class="props.class"
:ui="ui"
:ui-override="uiOverride"
:portal="portal"
:items="(item.children as T)"
align="start"
:align-offset="-4"
:side-offset="3"
:label-key="labelKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
v-bind="item.content"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData">
<slot :name="(name as keyof DropdownMenuContentSlots<T>)" v-bind="slotData" />
</template>
</UDropdownMenuContent>
</DropdownMenu.Sub>
<DropdownMenu.CheckboxItem
v-else-if="item.type === 'checkbox'"
:model-value="item.checked"
<DropdownMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<DropdownMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
<ReuseItemTemplate :item="item" :index="index" />
</DropdownMenu.Label>
<DropdownMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
<DropdownMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
<DropdownMenu.SubTrigger
as="button"
type="button"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
@update:model-value="item.onUpdateChecked"
@select="item.onSelect"
>
<ReuseItemTemplate :item="item" :index="index" />
</DropdownMenu.CheckboxItem>
<DropdownMenu.Item
v-else
as-child
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
@select="item.onSelect"
</DropdownMenu.SubTrigger>
<UDropdownMenuContent
sub
:class="props.class"
:ui="ui"
:ui-override="uiOverride"
:portal="portal"
:items="(item.children as T)"
align="start"
:align-offset="-4"
:side-offset="3"
:label-key="labelKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
v-bind="item.content"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<DropdownMenuItem, 'type'>)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color, active })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />
</ULinkBase>
</ULink>
</DropdownMenu.Item>
</template>
</DropdownMenu.Group>
</div>
<template v-for="(_, name) in proxySlots" #[name]="slotData">
<slot :name="(name as keyof DropdownMenuContentSlots<T>)" v-bind="slotData" />
</template>
</UDropdownMenuContent>
</DropdownMenu.Sub>
<DropdownMenu.CheckboxItem
v-else-if="item.type === 'checkbox'"
:model-value="item.checked"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
@update:model-value="item.onUpdateChecked"
@select="item.onSelect"
>
<ReuseItemTemplate :item="item" :index="index" />
</DropdownMenu.CheckboxItem>
<DropdownMenu.Item
v-else
as-child
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
@select="item.onSelect"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<DropdownMenuItem, 'type'>)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color, active })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />
</ULinkBase>
</ULink>
</DropdownMenu.Item>
</template>
</DropdownMenu.Group>
<slot />

View File

@@ -2,12 +2,12 @@
import type { DeepReadonly } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData } from '../types/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput } from '../types/form'
import type { ComponentConfig } from '../types/utils'
type FormConfig = ComponentConfig<typeof theme, AppConfig, 'form'>
export interface FormProps<S extends FormSchema, T extends boolean = true> {
export interface FormProps<S extends FormSchema> {
id?: string | number
/** Schema to validate the form state. Supports Standard Schema objects, Yup, Joi, and Superstructs. */
schema?: S
@@ -35,7 +35,7 @@ export interface FormProps<S extends FormSchema, T extends boolean = true> {
* If true, schema transformations will be applied to the state on submit.
* @defaultValue `true`
*/
transform?: T
transform?: boolean
/**
* If true, this form will attach to its parent Form (if any) and validate at the same time.
@@ -50,11 +50,11 @@ export interface FormProps<S extends FormSchema, T extends boolean = true> {
*/
loadingAuto?: boolean
class?: any
onSubmit?: ((event: FormSubmitEvent<FormData<S, T>>) => void | Promise<void>) | (() => void | Promise<void>)
onSubmit?: ((event: FormSubmitEvent<InferOutput<S>>) => void | Promise<void>) | (() => void | Promise<void>)
}
export interface FormEmits<S extends FormSchema, T extends boolean = true> {
(e: 'submit', payload: FormSubmitEvent<FormData<S, T>>): void
export interface FormEmits<S extends FormSchema> {
(e: 'submit', payload: FormSubmitEvent<InferOutput<S>>): void
(e: 'error', payload: FormErrorEvent): void
}
@@ -63,7 +63,7 @@ export interface FormSlots {
}
</script>
<script lang="ts" setup generic="S extends FormSchema, T extends boolean = true">
<script lang="ts" setup generic="S extends FormSchema">
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
import { useEventBus } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -75,17 +75,17 @@ import { FormValidationException } from '../types/form'
type I = InferInput<S>
type O = InferOutput<S>
const props = withDefaults(defineProps<FormProps<S, T>>(), {
const props = withDefaults(defineProps<FormProps<S>>(), {
validateOn() {
return ['input', 'blur', 'change'] as FormInputEvents[]
},
validateOnInputDelay: 300,
attach: true,
transform: () => true as T,
transform: true,
loadingAuto: true
})
const emits = defineEmits<FormEmits<S, T>>()
const emits = defineEmits<FormEmits<S>>()
defineSlots<FormSlots>()
const appConfig = useAppConfig() as FormConfig['AppConfig']
@@ -183,10 +183,10 @@ async function getErrors(): Promise<FormErrorWithId[]> {
return resolveErrorIds(errs)
}
type ValidateOpts<Silent extends boolean, Transform extends boolean> = { name?: keyof I | (keyof I)[], silent?: Silent, nested?: boolean, transform?: Transform }
async function _validate<T extends boolean>(opts: ValidateOpts<false, T>): Promise<FormData<S, T>>
async function _validate<T extends boolean>(opts: ValidateOpts<true, T>): Promise<FormData<S, T> | false>
async function _validate<T extends boolean>(opts: ValidateOpts<boolean, boolean> = { silent: false, nested: true, transform: false }): Promise<FormData<S, T> | false> {
type ValidateOpts<Silent extends boolean> = { name?: keyof I | (keyof I)[], silent?: Silent, nested?: boolean, transform?: boolean }
async function _validate(opts: ValidateOpts<false>): Promise<O>
async function _validate(opts: ValidateOpts<true>): Promise<O | false>
async function _validate(opts: ValidateOpts<boolean> = { silent: false, nested: true, transform: false }): Promise<O | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof O)[]
const nestedValidatePromises = !names && opts.nested
@@ -227,7 +227,7 @@ async function _validate<T extends boolean>(opts: ValidateOpts<boolean, boolean>
Object.assign(props.state, transformedState.value)
}
return props.state as FormData<S, T>
return props.state as O
}
const loading = ref(false)
@@ -236,7 +236,7 @@ provide(formLoadingInjectionKey, readonly(loading))
async function onSubmitWrapper(payload: Event) {
loading.value = props.loadingAuto && true
const event = payload as FormSubmitEvent<FormData<S, T>>
const event = payload as FormSubmitEvent<O>
try {
event.data = await _validate({ nested: true, transform: props.transform })
@@ -265,7 +265,7 @@ provide(formOptionsInjectionKey, computed(() => ({
validateOnInputDelay: props.validateOnInputDelay
})))
defineExpose<Form<S>>({
defineExpose<Form<I>>({
validate: _validate,
errors,

View File

@@ -38,13 +38,6 @@ export interface InputProps extends UseComponentIconsProps {
disabled?: boolean
/** Highlight the ring color like a focus state. */
highlight?: boolean
modelModifiers?: {
string?: boolean
number?: boolean
trim?: boolean
lazy?: boolean
nullify?: boolean
}
class?: any
ui?: Input['slots']
}
@@ -84,7 +77,6 @@ const props = withDefaults(defineProps<InputProps>(), {
const emits = defineEmits<InputEmits<T>>()
const slots = defineSlots<InputSlots>()
// eslint-disable-next-line vue/no-dupe-keys
const [modelValue, modelModifiers] = defineModel<T>()
const appConfig = useAppConfig() as Input['AppConfig']

View File

@@ -172,7 +172,7 @@ export interface InputMenuSlots<
<script setup lang="ts" generic="T extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
import { computed, ref, toRef, onMounted, toRaw } from 'vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits, useFilter } from 'reka-ui'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits, useFilter } from 'reka-ui'
import { defu } from 'defu'
import { isEqual } from 'ohash/utils'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
@@ -490,7 +490,7 @@ defineExpose({
</slot>
</ComboboxEmpty>
<div role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'top'" />
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
@@ -541,7 +541,7 @@ defineExpose({
</ComboboxGroup>
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'bottom'" />
</div>
</ComboboxViewport>
<slot name="content-bottom" />

View File

@@ -7,7 +7,7 @@ import type { ComponentConfig } from '../types/utils'
type InputNumber = ComponentConfig<typeof theme, AppConfig, 'inputNumber'>
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'stepSnapping' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions' | 'disableWheelChange' | 'invertWheelChange'> {
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'stepSnapping' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions' | 'disableWheelChange'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -98,7 +98,7 @@ defineSlots<InputNumberSlots>()
const { t, code: codeLocale } = useLocale()
const appConfig = useAppConfig() as InputNumber['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange', 'invertWheelChange'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange'), emits)
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
const { orientation, size: buttonGroupSize } = useButtonGroup<InputNumberProps>(props)

View File

@@ -55,19 +55,18 @@ export interface ModalProps extends DialogRootProps {
export interface ModalEmits extends DialogRootEmits {
'after:leave': []
'after:enter': []
'close:prevent': []
}
export interface ModalSlots {
default(props: { open: boolean }): any
content(props: { close: () => void }): any
header(props: { close: () => void }): any
content(props?: {}): any
header(props?: {}): any
title(props?: {}): any
description(props?: {}): any
close(props: { close: () => void, ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any
body(props: { close: () => void }): any
footer(props: { close: () => void }): any
close(props: { ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any
body(props?: {}): any
footer(props?: {}): any
}
</script>
@@ -124,9 +123,8 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
}))
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<DialogRoot v-slot="{ open, close }" v-bind="rootProps">
<DialogRoot v-slot="{ open }" v-bind="rootProps">
<DialogTrigger v-if="!!slots.default" as-child :class="props.class">
<slot :open="open" />
</DialogTrigger>
@@ -134,7 +132,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
<DialogPortal v-bind="portalProps">
<DialogOverlay v-if="overlay" :class="ui.overlay({ class: props.ui?.overlay })" />
<DialogContent :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-bind="contentProps" @after-enter="emits('after:enter')" @after-leave="emits('after:leave')" v-on="contentEvents">
<DialogContent :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-bind="contentProps" @after-leave="emits('after:leave')" v-on="contentEvents">
<VisuallyHidden v-if="!!slots.content && ((title || !!slots.title) || (description || !!slots.description))">
<DialogTitle v-if="title || !!slots.title">
<slot name="title">
@@ -149,9 +147,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</DialogDescription>
</VisuallyHidden>
<slot name="content" :close="close">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (props.close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header" :close="close">
<slot name="content">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header">
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
<DialogTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })">
<slot name="title">
@@ -166,16 +164,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</DialogDescription>
</div>
<DialogClose v-if="props.close || !!slots.close" as-child>
<slot name="close" :close="close" :ui="ui">
<DialogClose v-if="close || !!slots.close" as-child>
<slot name="close" :ui="ui">
<UButton
v-if="props.close"
v-if="close"
:icon="closeIcon || appConfig.ui.icons.close"
size="md"
color="neutral"
variant="ghost"
:aria-label="t('modal.close')"
v-bind="(typeof props.close === 'object' ? props.close as Partial<ButtonProps> : {})"
v-bind="(typeof close === 'object' ? close as Partial<ButtonProps> : {})"
:class="ui.close({ class: props.ui?.close })"
/>
</slot>
@@ -184,11 +182,11 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</div>
<div v-if="!!slots.body" :class="ui.body({ class: props.ui?.body })">
<slot name="body" :close="close" />
<slot name="body" />
</div>
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
<slot name="footer" :close="close" />
<slot name="footer" />
</div>
</slot>
</DialogContent>

View File

@@ -1,9 +1,9 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, NavigationMenuContentEmits, AccordionRootProps } from 'reka-ui'
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, NavigationMenuContentEmits, CollapsibleRootProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/navigation-menu'
import type { AvatarProps, BadgeProps, LinkProps, PopoverProps, TooltipProps } from '../types'
import type { AvatarProps, BadgeProps, LinkProps, TooltipProps } from '../types'
import type { ArrayOrNested, DynamicSlots, MergeTypes, NestedItem, EmitsToProps, ComponentConfig } from '../types/utils'
type NavigationMenu = ComponentConfig<typeof theme, AppConfig, 'navigationMenu'>
@@ -14,7 +14,7 @@ export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type'
[key: string]: any
}
export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
label?: string
/**
* @IconifyIcon
@@ -27,42 +27,41 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
*/
badge?: string | number | BadgeProps
/**
* Display a tooltip on the item when the menu is collapsed with the label of the item.
* This has priority over the global `tooltip` prop.
* Display a tooltip on the item.
* Only works when `type` is `link`.
* `{ content: { side: 'right' } }`{lang="ts-type"}
*/
tooltip?: boolean | TooltipProps
/**
* Display a popover on the item when the menu is collapsed with the children list.
* This has priority over the global `popover` prop.
*/
popover?: boolean | PopoverProps
tooltip?: TooltipProps
/**
* @IconifyIcon
*/
trailingIcon?: string
/**
* The type of the item.
* The `label` type is only displayed in `vertical` orientation.
* The `trigger` type is used to force the item to be collapsible when its a link in `vertical` orientation.
* The `label` type only works on `vertical` orientation.
* @defaultValue 'link'
*/
type?: 'label' | 'trigger' | 'link'
type?: 'label' | 'link'
slot?: string
/**
* The value of the item. Avoid using `index` as the value to prevent conflicts in horizontal orientation with Reka UI.
* @defaultValue `item-${index}`
*/
value?: string
/**
* Make the item collapsible.
* Only works when `orientation` is `vertical`.
* @defaultValue true
*/
collapsible?: boolean
children?: NavigationMenuChildItem[]
defaultOpen?: boolean
open?: boolean
onSelect?(e: Event): void
class?: any
ui?: Pick<NavigationMenu['slots'], 'item' | 'linkLeadingAvatarSize' | 'linkLeadingAvatar' | 'linkLeadingIcon' | 'linkLabel' | 'linkLabelExternalIcon' | 'linkTrailing' | 'linkTrailingBadgeSize' | 'linkTrailingBadge' | 'linkTrailingIcon' | 'label' | 'link' | 'content' | 'childList' | 'childLabel' | 'childItem' | 'childLink' | 'childLinkIcon' | 'childLinkWrapper' | 'childLinkLabel' | 'childLinkLabelExternalIcon' | 'childLinkDescription'>
ui?: Pick<NavigationMenu['slots'], 'item' | 'linkLeadingAvatarSize' | 'linkLeadingAvatar' | 'linkLeadingIcon' | 'linkLabel' | 'linkLabelExternalIcon' | 'linkTrailing' | 'linkTrailingBadgeSize' | 'linkTrailingBadge' | 'linkTrailingIcon' | 'label' | 'link' | 'content' | 'childList' | 'childItem' | 'childLink' | 'childLinkIcon' | 'childLinkWrapper' | 'childLinkLabel' | 'childLinkLabelExternalIcon' | 'childLinkDescription'>
[key: string]: any
}
export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'>, Pick<AccordionRootProps, 'disabled' | 'type' | 'collapsible'> {
export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -101,18 +100,6 @@ export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem>
* @defaultValue false
*/
collapsed?: boolean
/**
* Display a tooltip on the items when the menu is collapsed with the label of the item.
* `{ delayDuration: 0, content: { side: 'right' } }`{lang="ts-type"}
* @defaultValue false
*/
tooltip?: boolean | TooltipProps
/**
* Display a popover on the items when the menu is collapsed with the children list.
* `{ mode: 'hover', content: { side: 'right', align: 'start', alignOffset: 2 } }`{lang="ts-type"}
* @defaultValue false
*/
popover?: boolean | PopoverProps
/** Display a line next to the active item. */
highlight?: boolean
/**
@@ -162,9 +149,8 @@ export type NavigationMenuSlots<
<script setup lang="ts" generic="T extends ArrayOrNested<NavigationMenuItem>">
import { computed, toRef } from 'vue'
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, AccordionRoot, AccordionItem, AccordionTrigger, AccordionContent, useForwardPropsEmits } from 'reka-ui'
import { defu } from 'defu'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
import { createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { get, isArrayOfArray } from '../utils'
import { tv } from '../utils/tv'
@@ -174,7 +160,7 @@ import ULink from './Link.vue'
import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue'
import UBadge from './Badge.vue'
import UPopover from './Popover.vue'
import UCollapsible from './Collapsible.vue'
import UTooltip from './Tooltip.vue'
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
@@ -182,8 +168,6 @@ const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
contentOrientation: 'horizontal',
externalIcon: true,
delayDuration: 0,
type: 'multiple',
collapsible: true,
unmountOnHide: true,
labelKey: 'label'
})
@@ -204,10 +188,7 @@ const rootProps = useForwardPropsEmits(computed(() => ({
disablePointerLeaveClose: props.disablePointerLeaveClose,
unmountOnHide: props.unmountOnHide
})), emits)
const accordionProps = useForwardPropsEmits(reactivePick(props, 'collapsible', 'disabled', 'type', 'unmountOnHide'), emits)
const contentProps = toRef(() => props.content)
const tooltipProps = toRef(() => defu(typeof props.tooltip === 'boolean' ? {} : props.tooltip, { delayDuration: 0, content: { side: 'right' } }) as TooltipProps)
const popoverProps = toRef(() => defu(typeof props.popover === 'boolean' ? {} : props.popover, { mode: 'hover', content: { side: 'right', align: 'start', alignOffset: 2 } }) as PopoverProps)
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, level?: number }>({
@@ -220,7 +201,7 @@ const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: N
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.navigationMenu || {}) })({
orientation: props.orientation,
contentOrientation: props.orientation === 'vertical' ? undefined : props.contentOrientation,
contentOrientation: props.contentOrientation,
collapsed: props.collapsed,
color: props.color,
variant: props.variant,
@@ -235,24 +216,6 @@ const lists = computed<NavigationMenuItem[][]>(() =>
: [props.items]
: []
)
function getAccordionDefaultValue(list: NavigationMenuItem[]) {
function findItemsWithDefaultOpen(items: NavigationMenuItem[], level = 0): string[] {
return items.reduce((acc: string[], item, index) => {
if (item.defaultOpen || item.open) {
acc.push(item.value || (level > 0 ? `item-${level}-${index}` : `item-${index}`))
}
if (item.children?.length) {
acc.push(...findItemsWithDefaultOpen(item.children, level + 1))
}
return acc
}, [])
}
const indexes = findItemsWithDefaultOpen(list)
return props.type === 'single' ? indexes[0] : indexes
}
</script>
<template>
@@ -274,7 +237,7 @@ function getAccordionDefaultValue(list: NavigationMenuItem[]) {
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: [props.ui?.linkLabelExternalIcon, item.ui?.linkLabelExternalIcon], active })" />
</span>
<component :is="orientation === 'vertical' && item.children?.length && !collapsed ? AccordionTrigger : 'span'" v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" as="span" :class="ui.linkTrailing({ class: [props.ui?.linkTrailing, item.ui?.linkTrailing] })" @click.stop.prevent>
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" :class="ui.linkTrailing({ class: [props.ui?.linkTrailing, item.ui?.linkTrailing] })">
<slot :name="((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
<UBadge
v-if="item.badge"
@@ -285,62 +248,36 @@ function getAccordionDefaultValue(list: NavigationMenuItem[]) {
:class="ui.linkTrailingBadge({ class: [props.ui?.linkTrailingBadge, item.ui?.linkTrailingBadge] })"
/>
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon], active })" />
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length && item.collapsible !== false)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon], active })" />
<UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon], active })" />
</slot>
</component>
</span>
</slot>
</DefineLinkTemplate>
<DefineItemTemplate v-slot="{ item, index, level = 0 }">
<component
:is="(orientation === 'vertical' && !collapsed) ? AccordionItem : NavigationMenuItem"
:is="(orientation === 'vertical' && item.children?.length) ? UCollapsible : NavigationMenuItem"
as="li"
:value="item.value || (level > 0 ? `item-${level}-${index}` : `item-${index}`)"
:value="item.value || `item-${index}`"
:default-open="item.defaultOpen"
:disabled="(orientation === 'vertical' && item.children?.length) ? item.collapsible === false : undefined"
:unmount-on-hide="(orientation === 'vertical' && item.children?.length) ? unmountOnHide : undefined"
:open="item.open"
>
<div v-if="orientation === 'vertical' && item.type === 'label' && !collapsed" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })">
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })">
<ReuseLinkTemplate :item="item" :index="index" />
</div>
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && !collapsed && item.type === 'trigger') ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && item.collapsible !== false) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
<component
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : ((orientation === 'vertical' && item.children?.length && !collapsed && !(slotProps as any).href) ? AccordionTrigger : NavigationMenuLink)"
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : NavigationMenuLink"
as-child
:active="active || item.active"
:disabled="item.disabled"
@select="item.onSelect"
>
<UPopover v-if="orientation === 'vertical' && collapsed && item.children?.length && (!!props.popover || !!item.popover)" v-bind="{ ...popoverProps, ...(typeof item.popover === 'boolean' ? {} : item.popover || {}) }" :ui="{ content: ui.content({ class: [props.ui?.content, item.ui?.content] }) }">
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: level > 0 })">
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
</ULinkBase>
<template #content>
<slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active || item.active" :index="index">
<ul :class="ui.childList({ class: [props.ui?.childList, item.ui?.childList] })">
<li :class="ui.childLabel({ class: [props.ui?.childLabel, item.ui?.childLabel] })">
{{ get(item, props.labelKey as string) }}
</li>
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: [props.ui?.childItem, item.ui?.childItem] })">
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
<NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, item.ui?.childLink, childItem.class], active: childActive })">
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: [props.ui?.childLinkIcon, item.ui?.childLinkIcon], active: childActive })" />
<span :class="ui.childLinkLabel({ class: [props.ui?.childLinkLabel, item.ui?.childLinkLabel], active: childActive })">
{{ get(childItem, props.labelKey as string) }}
<UIcon v-if="childItem.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: [props.ui?.childLinkLabelExternalIcon, item.ui?.childLinkLabelExternalIcon], active: childActive })" />
</span>
</ULinkBase>
</NavigationMenuLink>
</ULink>
</li>
</ul>
</slot>
</template>
</UPopover>
<UTooltip v-else-if="orientation === 'vertical' && collapsed && (!!props.tooltip || !!item.tooltip)" :text="get(item, props.labelKey as string)" v-bind="{ ...tooltipProps, ...(typeof item.tooltip === 'boolean' ? {} : item.tooltip || {}) }">
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: level > 0 })">
<UTooltip v-if="!!item.tooltip" :content="{ side: 'right' }" v-bind="item.tooltip">
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
</ULinkBase>
</UTooltip>
@@ -350,7 +287,7 @@ function getAccordionDefaultValue(list: NavigationMenuItem[]) {
</component>
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])" v-bind="contentProps" :class="ui.content({ class: [props.ui?.content, item.ui?.content] })">
<slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active || item.active" :index="index">
<slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
<ul :class="ui.childList({ class: [props.ui?.childList, item.ui?.childList] })">
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: [props.ui?.childItem, item.ui?.childItem] })">
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
@@ -377,7 +314,7 @@ function getAccordionDefaultValue(list: NavigationMenuItem[]) {
</NavigationMenuContent>
</ULink>
<AccordionContent v-if="orientation === 'vertical' && item.children?.length && !collapsed" :class="ui.content({ class: [props.ui?.content, item.ui?.content] })">
<template v-if="orientation === 'vertical' && item.children?.length " #content>
<ul :class="ui.childList({ class: props.ui?.childList })">
<ReuseItemTemplate
v-for="(childItem, childIndex) in item.children"
@@ -388,7 +325,7 @@ function getAccordionDefaultValue(list: NavigationMenuItem[]) {
:class="ui.childItem({ class: [props.ui?.childItem, childItem.ui?.childItem] })"
/>
</ul>
</AccordionContent>
</template>
</component>
</DefineItemTemplate>
@@ -396,17 +333,9 @@ function getAccordionDefaultValue(list: NavigationMenuItem[]) {
<slot name="list-leading" />
<template v-for="(list, listIndex) in lists" :key="`list-${listIndex}`">
<component
v-bind="orientation === 'vertical' && !collapsed ? {
...accordionProps,
defaultValue: getAccordionDefaultValue(list)
} : {}"
:is="orientation === 'vertical' && !collapsed ? AccordionRoot : NavigationMenuList"
as="ul"
:class="ui.list({ class: props.ui?.list })"
>
<NavigationMenuList :class="ui.list({ class: props.ui?.list })">
<ReuseItemTemplate v-for="(item, index) in list" :key="`list-${listIndex}-${index}`" :item="item" :index="index" :class="ui.item({ class: [props.ui?.item, item.ui?.item] })" />
</component>
</NavigationMenuList>
<div v-if="orientation === 'vertical' && listIndex < lists.length - 1" :class="ui.separator({ class: props.ui?.separator })" />
</template>

View File

@@ -2,13 +2,13 @@
import { computed } from 'vue'
import { useOverlay, type Overlay } from '../composables/useOverlay'
const { overlays, unmount, close } = useOverlay()
const { overlays, unMount, close } = useOverlay()
const mountedOverlays = computed(() => overlays.filter((overlay: Overlay) => overlay.isMounted))
const onAfterLeave = (id: symbol) => {
close(id)
unmount(id)
unMount(id)
}
const onClose = (id: symbol, value: any) => {

View File

@@ -7,9 +7,7 @@ import type { ComponentConfig } from '../types/utils'
type PinInput = ComponentConfig<typeof theme, AppConfig, 'pinInput'>
type PinInputType = 'text' | 'number'
export interface PinInputProps<T extends PinInputType = 'text'> extends Pick<PinInputRootProps<T>, 'defaultValue' | 'disabled' | 'id' | 'mask' | 'modelValue' | 'name' | 'otp' | 'placeholder' | 'required' | 'type'> {
export interface PinInputProps extends Pick<PinInputRootProps, 'defaultValue' | 'disabled' | 'id' | 'mask' | 'modelValue' | 'name' | 'otp' | 'placeholder' | 'required' | 'type'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -39,14 +37,14 @@ export interface PinInputProps<T extends PinInputType = 'text'> extends Pick<Pin
ui?: PinInput['slots']
}
export type PinInputEmits<T extends PinInputType = 'text'> = PinInputRootEmits<T> & {
export type PinInputEmits = PinInputRootEmits & {
change: [payload: Event]
blur: [payload: Event]
}
</script>
<script setup lang="ts" generic="T extends PinInputType = 'text'">
<script setup lang="ts">
import type { ComponentPublicInstance } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { PinInputInput, PinInputRoot, useForwardPropsEmits } from 'reka-ui'
@@ -56,16 +54,16 @@ import { useFormField } from '../composables/useFormField'
import { looseToNumber } from '../utils'
import { tv } from '../utils/tv'
const props = withDefaults(defineProps<PinInputProps<T>>(), {
type: 'text' as never,
const props = withDefaults(defineProps<PinInputProps>(), {
type: 'text',
length: 5,
autofocusDelay: 0
})
const emits = defineEmits<PinInputEmits<T>>()
const emits = defineEmits<PinInputEmits>()
const appConfig = useAppConfig() as PinInput['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'required', 'type'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'placeholder', 'required', 'type'), emits)
const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<PinInputProps>(props)
@@ -79,7 +77,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.pinInput ||
const inputsRef = ref<ComponentPublicInstance[]>([])
const completed = ref(false)
function onComplete(value: string[] | number[]) {
function onComplete(value: string[]) {
// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
@@ -115,7 +113,6 @@ defineExpose({
v-bind="{ ...rootProps, ...ariaAttrs }"
:id="id"
:name="name"
:placeholder="placeholder"
:class="ui.root({ class: [props.ui?.root, props.class] })"
@update:model-value="emitFormInput()"
@complete="onComplete"

View File

@@ -43,7 +43,6 @@ export interface PopoverEmits extends PopoverRootEmits {
export interface PopoverSlots {
default(props: { open: boolean }): any
content(props?: {}): any
anchor(props?: {}): any
}
</script>
@@ -104,10 +103,6 @@ const Component = computed(() => props.mode === 'hover' ? HoverCard : Popover)
<slot :open="open" />
</Component.Trigger>
<Component.Anchor v-if="'Anchor' in Component && !!slots.anchor" as-child>
<slot name="anchor" />
</Component.Anchor>
<Component.Portal v-bind="portalProps">
<Component.Content v-bind="contentProps" :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-on="contentEvents">
<slot name="content" />

View File

@@ -7,7 +7,7 @@ import type { ComponentConfig } from '../types/utils'
type Progress = ComponentConfig<typeof theme, AppConfig, 'progress'>
export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' | 'getValueText' | 'modelValue'> {
export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' | 'modelValue'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -70,7 +70,7 @@ const slots = defineSlots<ProgressSlots>()
const { dir } = useLocale()
const appConfig = useAppConfig() as Progress['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'getValueLabel', 'getValueText', 'modelValue'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'getValueLabel', 'modelValue'), emits)
const isIndeterminate = computed(() => rootProps.value.modelValue === null)
const hasSteps = computed(() => Array.isArray(props.max))

View File

@@ -135,7 +135,7 @@ export interface SelectSlots<
<script setup lang="ts" generic="T extends ArrayOrNested<SelectItem>, VK extends GetItemKeys<T> = 'value', M extends boolean = false">
import { computed, toRef } from 'vue'
import { SelectRoot, SelectArrow, SelectTrigger, SelectPortal, SelectContent, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'reka-ui'
import { SelectRoot, SelectArrow, SelectTrigger, SelectPortal, SelectContent, SelectViewport, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'reka-ui'
import { defu } from 'defu'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -193,10 +193,9 @@ const groups = computed<SelectItem[][]>(() =>
// eslint-disable-next-line vue/no-dupe-keys
const items = computed(() => groups.value.flatMap(group => group) as T[])
function displayValue(value: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string | undefined {
function displayValue(value?: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string {
if (props.multiple && Array.isArray(value)) {
const values = value.map(v => displayValue(v)).filter(Boolean)
return values?.length ? values.join(', ') : undefined
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
}
const item = items.value.find(item => compare(typeof item === 'object' ? get(item as Record<string, any>, props.valueKey as string) : item, value))
@@ -251,7 +250,7 @@ function isSelectItem(item: SelectItem): item is SelectItemBase {
<slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
<template v-for="displayedModelValue in [displayValue(modelValue as GetModelValue<T, VK, M>)]" :key="displayedModelValue">
<span v-if="displayedModelValue !== undefined && displayedModelValue !== null" :class="ui.value({ class: props.ui?.value })">
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
{{ displayedModelValue }}
</span>
<span v-else :class="ui.placeholder({ class: props.ui?.placeholder })">
@@ -271,7 +270,7 @@ function isSelectItem(item: SelectItem): item is SelectItemBase {
<SelectContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
<slot name="content-top" />
<div role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<SelectViewport :class="ui.viewport({ class: props.ui?.viewport })">
<SelectGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<SelectLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })">
@@ -318,7 +317,7 @@ function isSelectItem(item: SelectItem): item is SelectItemBase {
</SelectItem>
</template>
</SelectGroup>
</div>
</SelectViewport>
<slot name="content-bottom" />

View File

@@ -115,16 +115,6 @@ export interface SelectMenuProps<T extends ArrayOrNested<SelectMenuItem> = Array
* @defaultValue false
*/
ignoreFilter?: boolean
/**
* Estimated size (in px) of each item for virtualization.
* @defaultValue 35
*/
estimateSize?: number
/**
* Number of items rendered outside the visible area for virtualization.
* @defaultValue 5
*/
overscan?: number
class?: any
ui?: SelectMenu['slots']
}
@@ -176,7 +166,7 @@ export interface SelectMenuSlots<
<script setup lang="ts" generic="T extends ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
import { computed, toRef, toRaw } from 'vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, ComboboxViewport, ComboboxVirtualizer, FocusScope, useForwardPropsEmits, useFilter } from 'reka-ui'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, FocusScope, useForwardPropsEmits, useFilter } from 'reka-ui'
import { defu } from 'defu'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -199,9 +189,7 @@ const props = withDefaults(defineProps<SelectMenuProps<T, VK, M>>(), {
searchInput: true,
labelKey: 'label' as never,
resetSearchTermOnBlur: true,
resetSearchTermOnSelect: true,
estimateSize: 35,
overscan: 5
resetSearchTermOnSelect: true
})
const emits = defineEmits<SelectMenuEmits<T, VK, M>>()
const slots = defineSlots<SelectMenuSlots<T, VK, M>>()
@@ -237,10 +225,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.selectMenu |
buttonGroup: orientation.value
}))
function displayValue(value: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string | undefined {
function displayValue(value: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string {
if (props.multiple && Array.isArray(value)) {
const values = value.map(v => displayValue(v)).filter(Boolean)
return values?.length ? values.join(', ') : undefined
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
}
if (!props.valueKey) {
@@ -356,12 +343,6 @@ function onSelect(e: Event, item: SelectMenuItem) {
function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
return typeof item === 'object' && item !== null
}
function getItemTextContent(item: SelectMenuItem): string {
if (typeof item === 'string') return item
if (typeof item !== 'object' || item === null) return String(item)
return get(item, props.labelKey as string) || String(item)
}
</script>
<!-- eslint-disable vue/no-template-shadow -->
@@ -404,7 +385,7 @@ function getItemTextContent(item: SelectMenuItem): string {
<slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
<template v-for="displayedModelValue in [displayValue(modelValue as GetModelValue<T, VK, M>)]" :key="displayedModelValue">
<span v-if="displayedModelValue !== undefined && displayedModelValue !== null" :class="ui.value({ class: props.ui?.value })">
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
{{ displayedModelValue }}
</span>
<span v-else :class="ui.placeholder({ class: props.ui?.placeholder })">
@@ -439,57 +420,52 @@ function getItemTextContent(item: SelectMenuItem): string {
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'top'" />
<ComboboxVirtualizer
v-slot="{ option }"
:options="filteredItems as AcceptableValue[]"
:estimate-size="estimateSize"
:overscan="overscan"
:text-content="getItemTextContent"
:class="ui.group({ class: props.ui?.group })"
>
<ComboboxLabel v-if="isSelectItem(option) && option.type === 'label'" :class="ui.label({ class: [props.ui?.label, option.ui?.label, option.class] })">
{{ get(option, props.labelKey as string) }}
</ComboboxLabel>
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ComboboxLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })">
{{ get(item, props.labelKey as string) }}
</ComboboxLabel>
<ComboboxSeparator v-else-if="isSelectItem(option) && option.type === 'separator'" :class="ui.separator({ class: [props.ui?.separator, option.ui?.separator, option.class] })" />
<ComboboxSeparator v-else-if="isSelectItem(item) && item.type === 'separator'" :class="ui.separator({ class: [props.ui?.separator, item.ui?.separator, item.class] })" />
<ComboboxItem
v-else
:class="ui.item({ class: [props.ui?.item, isSelectItem(option) && option.ui?.item, isSelectItem(option) && option.class] })"
:disabled="isSelectItem(option) && option.disabled"
:value="props.valueKey && isSelectItem(option) ? get(option, props.valueKey as string) : option"
@select="onSelect($event, option)"
>
<slot name="item" :item="(option as NestedItem<T>)" :index="0">
<slot name="item-leading" :item="(option as NestedItem<T>)" :index="0">
<UIcon v-if="isSelectItem(option) && option.icon" :name="option.icon" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, option.ui?.itemLeadingIcon] })" />
<UAvatar v-else-if="isSelectItem(option) && option.avatar" :size="((option.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="option.avatar" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, option.ui?.itemLeadingAvatar] })" />
<UChip
v-else-if="isSelectItem(option) && option.chip"
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
inset
standalone
v-bind="option.chip"
:class="ui.itemLeadingChip({ class: [props.ui?.itemLeadingChip, option.ui?.itemLeadingChip] })"
/>
</slot>
<span :class="ui.itemLabel({ class: [props.ui?.itemLabel, isSelectItem(option) && option.ui?.itemLabel] })">
<slot name="item-label" :item="(option as NestedItem<T>)" :index="0">
{{ isSelectItem(option) ? get(option, props.labelKey as string) : option }}
<ComboboxItem
v-else
:class="ui.item({ class: [props.ui?.item, isSelectItem(item) && item.ui?.item, isSelectItem(item) && item.class] })"
:disabled="isSelectItem(item) && item.disabled"
:value="props.valueKey && isSelectItem(item) ? get(item, props.valueKey as string) : item"
@select="onSelect($event, item)"
>
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon] })" />
<UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, item.ui?.itemLeadingAvatar] })" />
<UChip
v-else-if="isSelectItem(item) && item.chip"
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
inset
standalone
v-bind="item.chip"
:class="ui.itemLeadingChip({ class: [props.ui?.itemLeadingChip, item.ui?.itemLeadingChip] })"
/>
</slot>
</span>
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, isSelectItem(option) && option.ui?.itemTrailing] })">
<slot name="item-trailing" :item="(option as NestedItem<T>)" :index="0" />
<span :class="ui.itemLabel({ class: [props.ui?.itemLabel, isSelectItem(item) && item.ui?.itemLabel] })">
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
</slot>
</span>
<ComboboxItemIndicator as-child>
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, isSelectItem(option) && option.ui?.itemTrailingIcon] })" />
</ComboboxItemIndicator>
</span>
</slot>
</ComboboxItem>
</ComboboxVirtualizer>
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, isSelectItem(item) && item.ui?.itemTrailing] })">
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
<ComboboxItemIndicator as-child>
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, isSelectItem(item) && item.ui?.itemTrailingIcon] })" />
</ComboboxItemIndicator>
</span>
</slot>
</ComboboxItem>
</template>
</ComboboxGroup>
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'bottom'" />
</ComboboxViewport>

View File

@@ -55,19 +55,18 @@ export interface SlideoverProps extends DialogRootProps {
export interface SlideoverEmits extends DialogRootEmits {
'after:leave': []
'after:enter': []
'close:prevent': []
}
export interface SlideoverSlots {
default(props: { open: boolean }): any
content(props: { close: () => void }): any
header(props: { close: () => void }): any
content(props?: {}): any
header(props?: {}): any
title(props?: {}): any
description(props?: {}): any
close(props: { close: () => void, ui: { [K in keyof Required<Slideover['slots']>]: (props?: Record<string, any>) => string } }): any
body(props: { close: () => void }): any
footer(props: { close: () => void }): any
close(props: { ui: { [K in keyof Required<Slideover['slots']>]: (props?: Record<string, any>) => string } }): any
body(props?: {}): any
footer(props?: {}): any
}
</script>
@@ -124,9 +123,8 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
}))
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<DialogRoot v-slot="{ open, close }" v-bind="rootProps">
<DialogRoot v-slot="{ open }" v-bind="rootProps">
<DialogTrigger v-if="!!slots.default" as-child :class="props.class">
<slot :open="open" />
</DialogTrigger>
@@ -134,14 +132,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
<DialogPortal v-bind="portalProps">
<DialogOverlay v-if="overlay" :class="ui.overlay({ class: props.ui?.overlay })" />
<DialogContent
:data-side="side"
:class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })"
v-bind="contentProps"
@after-enter="emits('after:enter')"
@after-leave="emits('after:leave')"
v-on="contentEvents"
>
<DialogContent :data-side="side" :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-bind="contentProps" @after-leave="emits('after:leave')" v-on="contentEvents">
<VisuallyHidden v-if="!!slots.content && ((title || !!slots.title) || (description || !!slots.description))">
<DialogTitle v-if="title || !!slots.title">
<slot name="title">
@@ -156,9 +147,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
</DialogDescription>
</VisuallyHidden>
<slot name="content" :close="close">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (props.close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header" :close="close">
<slot name="content">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header">
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
<DialogTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })">
<slot name="title">
@@ -173,16 +164,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
</DialogDescription>
</div>
<DialogClose v-if="props.close || !!slots.close" as-child>
<slot name="close" :close="close" :ui="ui">
<DialogClose v-if="close || !!slots.close" as-child>
<slot name="close" :ui="ui">
<UButton
v-if="props.close"
v-if="close"
:icon="closeIcon || appConfig.ui.icons.close"
size="md"
color="neutral"
variant="ghost"
:aria-label="t('slideover.close')"
v-bind="(typeof props.close === 'object' ? props.close as Partial<ButtonProps> : {})"
v-bind="(typeof close === 'object' ? close as Partial<ButtonProps> : {})"
:class="ui.close({ class: props.ui?.close })"
/>
</slot>
@@ -191,11 +182,11 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
</div>
<div :class="ui.body({ class: props.ui?.body })">
<slot name="body" :close="close" />
<slot name="body" />
</div>
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
<slot name="footer" :close="close" />
<slot name="footer" />
</div>
</slot>
</DialogContent>

View File

@@ -3,12 +3,10 @@ import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/textarea'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps } from '../types'
import type { ComponentConfig } from '../types/utils'
import type { AcceptableValue, ComponentConfig } from '../types/utils'
type Textarea = ComponentConfig<typeof theme, AppConfig, 'textarea'>
type TextareaValue = string | number | null
export interface TextareaProps extends UseComponentIconsProps {
/**
* The element or component this component should render as.
@@ -37,21 +35,15 @@ export interface TextareaProps extends UseComponentIconsProps {
autoresize?: boolean
autoresizeDelay?: number
disabled?: boolean
class?: any
rows?: number
maxrows?: number
/** Highlight the ring color like a focus state. */
highlight?: boolean
modelModifiers?: {
string?: boolean
trim?: boolean
lazy?: boolean
nullify?: boolean
}
class?: any
ui?: Textarea['slots']
}
export interface TextareaEmits<T extends TextareaValue = TextareaValue> {
export interface TextareaEmits<T extends AcceptableValue = AcceptableValue> {
(e: 'update:modelValue', payload: T): void
(e: 'blur', event: FocusEvent): void
(e: 'change', event: Event): void
@@ -64,7 +56,7 @@ export interface TextareaSlots {
}
</script>
<script setup lang="ts" generic="T extends TextareaValue">
<script setup lang="ts" generic="T extends AcceptableValue">
import { ref, computed, onMounted, nextTick, watch } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
@@ -72,8 +64,6 @@ import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { looseToNumber } from '../utils'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
defineOptions({ inheritAttrs: false })
@@ -86,7 +76,6 @@ const props = withDefaults(defineProps<TextareaProps>(), {
const emits = defineEmits<TextareaEmits<T>>()
const slots = defineSlots<TextareaSlots>()
// eslint-disable-next-line vue/no-dupe-keys
const [modelValue, modelModifiers] = defineModel<T>()
const appConfig = useAppConfig() as Textarea['AppConfig']

View File

@@ -1,129 +0,0 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/timeline'
import type { AvatarProps } from '../types'
import type { DynamicSlots, ComponentConfig } from '../types/utils'
type Timeline = ComponentConfig<typeof theme, AppConfig, 'timeline'>
export interface TimelineItem {
date?: string
title?: string
description?: string
icon?: string
avatar?: AvatarProps
value?: string | number
slot?: string
class?: any
ui?: Pick<Timeline['slots'], 'item' | 'container' | 'indicator' | 'separator' | 'wrapper' | 'date' | 'title' | 'description'>
[key: string]: any
}
export interface TimelineProps<T extends TimelineItem = TimelineItem> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
items: T[]
/**
* @defaultValue 'md'
*/
size?: Timeline['variants']['size']
/**
* @defaultValue 'primary'
*/
color?: Timeline['variants']['color']
/**
* The orientation of the Timeline.
* @defaultValue 'vertical'
*/
orientation?: Timeline['variants']['orientation']
defaultValue?: string | number
class?: any
ui?: Timeline['slots']
}
type SlotProps<T extends TimelineItem> = (props: { item: T }) => any
export type TimelineSlots<T extends TimelineItem = TimelineItem> = {
indicator: SlotProps<T>
date: SlotProps<T>
title: SlotProps<T>
description: SlotProps<T>
} & DynamicSlots<T, 'indicator' | 'date' | 'title' | 'description', { item: T }>
</script>
<script setup lang="ts" generic="T extends TimelineItem">
import { computed } from 'vue'
import { Primitive, Separator } from 'reka-ui'
import { useAppConfig } from '#imports'
import { tv } from '../utils/tv'
import UAvatar from './Avatar.vue'
const props = withDefaults(defineProps<TimelineProps<T>>(), {
orientation: 'vertical'
})
const slots = defineSlots<TimelineSlots<T>>()
const modelValue = defineModel<string | number>()
const appConfig = useAppConfig() as Timeline['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.timeline || {}) })({
orientation: props.orientation,
size: props.size,
color: props.color
}))
const currentStepIndex = computed(() => {
const value = modelValue.value ?? props.defaultValue
return ((typeof value === 'string')
? props.items.findIndex(item => item.value === value)
: value) ?? -1
})
</script>
<template>
<Primitive :as="as" :data-orientation="orientation" :class="ui.root({ class: [props.ui?.root, props.class] })">
<div
v-for="(item, index) in items"
:key="item.value ?? index"
:class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })"
:data-state="index < currentStepIndex ? 'completed' : index === currentStepIndex ? 'active' : undefined"
>
<div :class="ui.container({ class: [props.ui?.container, item.ui?.container] })">
<UAvatar :size="size" :icon="item.icon" v-bind="typeof item.avatar === 'object' ? item.avatar : {}" :class="ui.indicator({ class: [props.ui?.indicator, item.ui?.indicator] })" :ui="{ icon: 'text-inherit', fallback: 'text-inherit' }">
<slot :name="((item.slot ? `${item.slot}-indicator` : 'indicator') as keyof TimelineSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" />
</UAvatar>
<Separator
v-if="index < items.length - 1"
:class="ui.separator({ class: [props.ui?.separator, item.ui?.separator] })"
:orientation="props.orientation"
/>
</div>
<div :class="ui.wrapper({ class: [props.ui?.wrapper, item.ui?.wrapper] })">
<div v-if="item.date" :class="ui.date({ class: [props.ui?.date, item.ui?.date] })">
<slot :name="((item.slot ? `${item.slot}-date` : 'date') as keyof TimelineSlots<T>)" :item="(item as Extract<T, { slot: string; }>)">
{{ item.date }}
</slot>
</div>
<div v-if="item.title || !!slots.title" :class="ui.title({ class: [props.ui?.title, item.ui?.title] })">
<slot :name="((item.slot ? `${item.slot}-title` : 'title') as keyof TimelineSlots<T>)" :item="(item as Extract<T, { slot: string; }>)">
{{ item.title }}
</slot>
</div>
<div v-if="item.description || !!slots.description" :class="ui.description({ class: [props.ui?.description, item.ui?.description] })">
<slot :name="((item.slot ? `${item.slot}-description` : 'description') as keyof TimelineSlots<T>)" :item="(item as Extract<T, { slot: string; }>)">
{{ item.description }}
</slot>
</div>
</div>
</div>
</Primitive>
</template>

View File

@@ -29,7 +29,7 @@ export type TreeItem = {
[key: string]: any
}
export interface TreeProps<T extends TreeItem[] = TreeItem[], VK extends GetItemKeys<T> = 'value', M extends boolean = false> extends Pick<TreeRootProps<T>, 'expanded' | 'defaultExpanded' | 'selectionBehavior' | 'propagateSelect' | 'disabled' | 'bubbleSelect'> {
export interface TreeProps<T extends TreeItem[] = TreeItem[], VK extends GetItemKeys<T> = 'value', M extends boolean = false> extends Pick<TreeRootProps<T>, 'expanded' | 'defaultExpanded' | 'selectionBehavior' | 'propagateSelect' | 'disabled'> {
/**
* The element or component this component should render as.
* @defaultValue 'ul'
@@ -116,7 +116,7 @@ const slots = defineSlots<TreeSlots<T>>()
const appConfig = useAppConfig() as Tree['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'items', 'multiple', 'expanded', 'disabled', 'propagateSelect', 'bubbleSelect'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'items', 'multiple', 'expanded', 'disabled', 'propagateSelect'), emits)
const [DefineTreeTemplate, ReuseTreeTemplate] = createReusableTemplate<{ items?: TreeItem[], level: number }, TreeSlots<T>>()

View File

@@ -93,7 +93,7 @@ function _useOverlay() {
overlays.forEach(overlay => close(overlay.id))
}
const unmount = (id: symbol): void => {
const unMount = (id: symbol): void => {
const overlay = getOverlay(id)
overlay.isMounted = false
@@ -135,7 +135,7 @@ function _useOverlay() {
closeAll,
create,
patch,
unmount,
unMount,
isOpen
}
}

View File

@@ -21,13 +21,9 @@ export { default as hy } from './hy'
export { default as id } from './id'
export { default as it } from './it'
export { default as ja } from './ja'
export { default as kk } from './kk'
export { default as km } from './km'
export { default as kk } from './kk'
export { default as ko } from './ko'
export { default as ky } from './ky'
export { default as lt } from './lt'
export { default as mn } from './mn'
export { default as ms } from './ms'
export { default as nb_no } from './nb_no'
export { default as nl } from './nl'
export { default as pl } from './pl'
@@ -41,8 +37,8 @@ export { default as sv } from './sv'
export { default as th } from './th'
export { default as tj } from './tj'
export { default as tr } from './tr'
export { default as ug_cn } from './ug_cn'
export { default as uk } from './uk'
export { default as ug_cn } from './ug_cn'
export { default as ur } from './ur'
export { default as uz } from './uz'
export { default as vi } from './vi'

View File

@@ -1,56 +0,0 @@
import type { Messages } from '../types'
import { defineLocale } from '../composables/defineLocale'
export default defineLocale<Messages>({
name: 'Кыргызча',
code: 'ky',
messages: {
inputMenu: {
noMatch: 'Эч нерсе табылган жок',
noData: 'Маалымат жок',
create: '"{label}" жасоо'
},
calendar: {
prevYear: 'Алдыңкы жыл',
nextYear: 'Кийинки жыл',
prevMonth: 'Алдыңкы ай',
nextMonth: 'Кийинки ай'
},
inputNumber: {
increment: 'Кошуу',
decrement: 'Азайтуу'
},
commandPalette: {
placeholder: 'Буйрук киргизиңиз же издөө…',
noMatch: 'Эч нерсе табылган жок',
noData: 'Маалымат жок',
close: 'Жабуу'
},
selectMenu: {
noMatch: 'Сүйлөшкөн маалыматтар жок',
noData: 'Маалымат жок',
create: '"{label}" жасоо',
search: 'Издөө...'
},
toast: {
close: 'Жабуу'
},
carousel: {
prev: 'Алдыңкы',
next: 'Кийинки',
goto: '{slide} слайдга өтүү'
},
modal: {
close: 'Жабуу'
},
slideover: {
close: 'Жабуу'
},
alert: {
close: 'Жабуу'
},
table: {
noData: 'Маалымат жок'
}
}
})

View File

@@ -1,56 +0,0 @@
import type { Messages } from '../types'
import { defineLocale } from '../composables/defineLocale'
export default defineLocale<Messages>({
name: 'Lietuvių',
code: 'lt',
messages: {
inputMenu: {
noMatch: 'Nėra atitinkančių duomenų',
noData: 'Nėra duomenų',
create: 'Sukurti „{label}“'
},
calendar: {
prevYear: 'Ankstesni metai',
nextYear: 'Kiti metai',
prevMonth: 'Ankstesnis mėnuo',
nextMonth: 'Kitas mėnuo'
},
inputNumber: {
increment: 'Padidinti',
decrement: 'Sumažinti'
},
commandPalette: {
placeholder: 'Įveskite komandą arba ieškokite...',
noMatch: 'Nėra atitinkančių duomenų',
noData: 'Nėra duomenų',
close: 'Uždaryti'
},
selectMenu: {
noMatch: 'Nėra atitinkančių duomenų',
noData: 'Nėra duomenų',
create: 'Sukurti „{label}“',
search: 'Ieškoti...'
},
toast: {
close: 'Uždaryti'
},
carousel: {
prev: 'Atgal',
next: 'Pirmyn',
goto: 'Eiti į skaidrę {slide}'
},
modal: {
close: 'Uždaryti'
},
slideover: {
close: 'Uždaryti'
},
alert: {
close: 'Uždaryti'
},
table: {
noData: 'Nėra duomenų'
}
}
})

View File

@@ -1,56 +0,0 @@
import type { Messages } from '../types'
import { defineLocale } from '../composables/defineLocale'
export default defineLocale<Messages>({
name: 'Монгол',
code: 'mn',
messages: {
inputMenu: {
noMatch: 'Тохирох мэдээлэл олдсонгүй',
noData: 'Мэдээлэл байхгүй',
create: '"{label}" үүсгэх'
},
calendar: {
prevYear: 'Өмнөх жил',
nextYear: 'Дараа жил',
prevMonth: 'Өмнөх сар',
nextMonth: 'Дараа сар'
},
inputNumber: {
increment: 'Нэмэх',
decrement: 'Хасах'
},
commandPalette: {
placeholder: 'Комманд бичих эсвэл хайлт хийх...',
noMatch: 'Тохирох мэдээлэл олдсонгүй',
noData: 'Мэдээлэл байхгүй',
close: 'Хаах'
},
selectMenu: {
noMatch: 'Тохирох мэдээлэл олдсонгүй',
noData: 'Мэдээлэл байхгүй',
create: '"{label}" үүсгэх',
search: 'Хайх...'
},
toast: {
close: 'Хаах'
},
carousel: {
prev: 'Өмнөх',
next: 'Дараах',
goto: '{slide}-р хуудсанд шилжих'
},
modal: {
close: 'Хаах'
},
slideover: {
close: 'Хаах'
},
alert: {
close: 'Хаах'
},
table: {
noData: 'Мэдээлэл байхгүй'
}
}
})

View File

@@ -1,56 +0,0 @@
import type { Messages } from '../types'
import { defineLocale } from '../composables/defineLocale'
export default defineLocale<Messages>({
name: 'Melayu',
code: 'ms',
messages: {
inputMenu: {
noMatch: 'Tiada data yang sepadan',
noData: 'Tiada data',
create: 'Cipta "{label}"'
},
calendar: {
prevYear: 'Tahun sebelum',
nextYear: 'Tahun seterusnya',
prevMonth: 'Bulan sebelum',
nextMonth: 'Bulan seterusnya'
},
inputNumber: {
increment: 'Naikkan',
decrement: 'Kurangkan'
},
commandPalette: {
placeholder: 'Taip arahan atau carian...',
noMatch: 'Tiada data yang sepadan',
noData: 'Tiada data',
close: 'Tutup'
},
selectMenu: {
noMatch: 'Tiada data yang sepadan',
noData: 'Tiada data',
create: 'Cipta "{label}"',
search: 'Cari...'
},
toast: {
close: 'Tutup'
},
carousel: {
prev: 'Sebelum',
next: 'Seterusnya',
goto: 'Pergi ke slaid {slide}'
},
modal: {
close: 'Tutup'
},
slideover: {
close: 'Tutup'
},
alert: {
close: 'Tutup'
},
table: {
noData: 'Tiada data'
}
}
})

View File

@@ -5,20 +5,20 @@ import type { ObjectSchema as YupObjectSchema } from 'yup'
import type { GetObjectField } from './utils'
import type { Struct as SuperstructSchema } from 'superstruct'
export interface Form<S extends FormSchema> {
validate<T extends boolean>(opts?: { name?: keyof FormData<S, false> | (keyof FormData<S, false>)[], silent?: boolean, nested?: boolean, transform?: T }): Promise<FormData<S, T> | false>
export interface Form<T extends object> {
validate (opts?: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean }): Promise<T | false>
clear (path?: string): void
errors: Ref<FormError[]>
setErrors (errs: FormError[], name?: keyof FormData<S, false>): void
getErrors (name?: keyof FormData<S, false>): FormError[]
setErrors (errs: FormError[], name?: keyof T): void
getErrors (name?: keyof T): FormError[]
submit (): Promise<void>
disabled: ComputedRef<boolean>
dirty: ComputedRef<boolean>
loading: Ref<boolean>
dirtyFields: DeepReadonly<Set<keyof FormData<S, false>>>
touchedFields: DeepReadonly<Set<keyof FormData<S, false>>>
blurredFields: DeepReadonly<Set<keyof FormData<S, false>>>
dirtyFields: DeepReadonly<Set<keyof T>>
touchedFields: DeepReadonly<Set<keyof T>>
blurredFields: DeepReadonly<Set<keyof T>>
}
export type FormSchema<I extends object = object, O extends object = I> =
@@ -42,8 +42,6 @@ export type InferOutput<Schema> = Schema extends StandardSchemaV1 ? StandardSche
: Schema extends SuperstructSchema<infer O, any> ? O
: never
export type FormData<S extends FormSchema, T extends boolean = true> = T extends true ? InferOutput<S> : InferInput<S>
export type FormInputEvents = 'input' | 'blur' | 'change' | 'focus'
export interface FormError<P extends string = string> {

View File

@@ -46,7 +46,6 @@ export * from '../components/Switch.vue'
export * from '../components/Table.vue'
export * from '../components/Tabs.vue'
export * from '../components/Textarea.vue'
export * from '../components/Timeline.vue'
export * from '../components/Toast.vue'
export * from '../components/Toaster.vue'
export * from '../components/Tooltip.vue'

View File

@@ -76,7 +76,7 @@ export function getTemplates(options: ModuleOptions, uiConfig: Record<string, an
write: true,
getContents: () => `@source "./ui";
@theme static {
@theme default inline {
--color-old-neutral-50: ${colors.neutral[50]};
--color-old-neutral-100: ${colors.neutral[100]};
--color-old-neutral-200: ${colors.neutral[200]};
@@ -88,9 +88,6 @@ export function getTemplates(options: ModuleOptions, uiConfig: Record<string, an
--color-old-neutral-800: ${colors.neutral[800]};
--color-old-neutral-900: ${colors.neutral[900]};
--color-old-neutral-950: ${colors.neutral[950]};
}
@theme default inline {
${[...(options.theme?.colors || []).filter(color => !colors[color as keyof typeof colors]), 'neutral'].map(color => [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950].map(shade => `--color-${color}-${shade}: var(--ui-color-${color}-${shade});`).join('\n\t')).join('\n\t')}
${options.theme?.colors?.map(color => `--color-${color}: var(--ui-${color});`).join('\n\t')}
--radius-xs: calc(var(--ui-radius) * 0.5);

View File

@@ -1,6 +1,6 @@
export default {
slots: {
root: 'inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated',
root: 'inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated',
image: 'h-full w-full rounded-[inherit] object-cover',
fallback: 'font-medium leading-none text-muted truncate',
icon: 'text-muted shrink-0'

View File

@@ -30,7 +30,7 @@ export default (options: Required<ModuleOptions>) => ({
},
active: {
true: {
dot: 'data-[state=active]:bg-inverted'
dot: 'bg-inverted'
}
}
}

View File

@@ -65,7 +65,7 @@ export default (options: Required<ModuleOptions>) => ({
orientation: 'horizontal',
variant: 'table',
class: {
item: 'first-of-type:rounded-s-lg last-of-type:rounded-e-lg',
item: 'first-of-type:rounded-l-lg last-of-type:rounded-r-lg',
fieldset: 'gap-0 -space-x-px'
}
},

View File

@@ -2,8 +2,7 @@ import type { ModuleOptions } from '../module'
export default (options: Required<ModuleOptions>) => ({
slots: {
content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-context-menu-content-transform-origin) flex flex-col',
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default divide-y divide-default overflow-y-auto scroll-py-1 data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-context-menu-content-transform-origin)',
group: 'p-1 isolate',
label: 'w-full flex items-center font-semibold text-highlighted',
separator: '-mx-1 my-1 h-px bg-border',

View File

@@ -2,8 +2,7 @@ import type { ModuleOptions } from '../module'
export default (options: Required<ModuleOptions>) => ({
slots: {
content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-dropdown-menu-content-transform-origin) flex flex-col',
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default divide-y divide-default overflow-y-auto scroll-py-1 data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-dropdown-menu-content-transform-origin)',
arrow: 'fill-default',
group: 'p-1 isolate',
label: 'w-full flex items-center font-semibold text-highlighted',

View File

@@ -13,7 +13,7 @@ export default {
external: 'i-lucide-arrow-up-right',
folder: 'i-lucide-folder',
folderOpen: 'i-lucide-folder-open',
loading: 'i-lucide-loader-circle',
loading: 'i-lucide-refresh-cw',
minus: 'i-lucide-minus',
plus: 'i-lucide-plus',
search: 'i-lucide-search'

View File

@@ -44,7 +44,6 @@ export { default as switch } from './switch'
export { default as table } from './table'
export { default as tabs } from './tabs'
export { default as textarea } from './textarea'
export { default as timeline } from './timeline'
export { default as toast } from './toast'
export { default as toaster } from './toaster'
export { default as tooltip } from './tooltip'

View File

@@ -8,8 +8,8 @@ export default (options: Required<ModuleOptions>) => {
base: () => ['rounded-md', options.theme.transitions && 'transition-colors'],
trailing: 'group absolute inset-y-0 end-0 flex items-center disabled:cursor-not-allowed disabled:opacity-75',
arrow: 'fill-default',
content: 'max-h-60 w-(--reka-combobox-trigger-width) bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-combobox-content-transform-origin) pointer-events-auto flex flex-col',
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
content: 'max-h-60 w-(--reka-combobox-trigger-width) bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-combobox-content-transform-origin) pointer-events-auto',
viewport: 'divide-y divide-default scroll-py-1',
group: 'p-1 isolate',
empty: 'py-2 text-center text-sm text-muted',
label: 'font-semibold text-highlighted',

View File

@@ -10,25 +10,24 @@ export default (options: Required<ModuleOptions>) => ({
linkLeadingIcon: 'shrink-0 size-5',
linkLeadingAvatar: 'shrink-0',
linkLeadingAvatarSize: '2xs',
linkTrailing: 'group ms-auto inline-flex gap-1.5 items-center',
linkTrailing: 'ms-auto inline-flex gap-1.5 items-center',
linkTrailingBadge: 'shrink-0',
linkTrailingBadgeSize: 'sm',
linkTrailingIcon: 'size-5 transform shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
linkLabel: 'truncate',
linkLabelExternalIcon: 'inline-block size-3 align-top text-dimmed',
childList: 'isolate',
childLabel: 'text-xs text-highlighted',
childList: '',
childItem: '',
childLink: 'group relative size-full flex items-start text-start text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2',
childLinkWrapper: 'min-w-0',
childLink: 'group size-full px-3 py-2 rounded-md flex items-start gap-2 text-start',
childLinkWrapper: 'flex flex-col items-start',
childLinkIcon: 'size-5 shrink-0',
childLinkLabel: 'truncate',
childLinkLabel: 'font-semibold text-sm relative inline-flex',
childLinkLabelExternalIcon: 'inline-block size-3 align-top text-dimmed',
childLinkDescription: 'text-muted',
childLinkDescription: 'text-sm text-muted',
separator: 'px-2 h-px bg-border',
viewportWrapper: 'absolute top-full left-0 flex w-full',
viewport: 'relative overflow-hidden bg-default shadow-lg rounded-md ring ring-default h-(--reka-navigation-menu-viewport-height) w-full transition-[width,height,left] duration-200 origin-[top_center] data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] z-[1]',
content: '',
content: 'absolute top-0 left-0 w-full',
indicator: 'absolute data-[state=visible]:animate-[fade-in_100ms_ease-out] data-[state=hidden]:animate-[fade-out_100ms_ease-in] data-[state=hidden]:opacity-0 bottom-0 z-[2] w-(--reka-navigation-menu-indicator-size) translate-x-(--reka-navigation-menu-indicator-position) flex h-2.5 items-end justify-center overflow-hidden transition-[translate,width] duration-200',
arrow: 'relative top-[50%] size-2.5 rotate-45 border border-default bg-default z-[1] rounded-xs'
},
@@ -36,11 +35,11 @@ export default (options: Required<ModuleOptions>) => ({
color: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, {
link: `focus-visible:before:ring-${color}`,
childLink: `focus-visible:before:ring-${color}`
childLink: `focus-visible:outline-${color}`
}])),
neutral: {
link: 'focus-visible:before:ring-inverted',
childLink: 'focus-visible:before:ring-inverted'
childLink: 'focus-visible:outline-inverted'
}
},
highlightColor: {
@@ -57,16 +56,11 @@ export default (options: Required<ModuleOptions>) => ({
list: 'flex items-center',
item: 'py-2',
link: 'px-2.5 py-1.5 before:inset-x-px before:inset-y-0',
childList: 'grid p-2',
childLink: 'px-3 py-2 gap-2 before:inset-x-px before:inset-y-0',
childLinkLabel: 'font-medium',
content: 'absolute top-0 left-0 w-full max-h-[70vh] overflow-y-auto'
childList: 'grid p-2'
},
vertical: {
root: 'flex-col',
link: 'flex-row px-2.5 py-1.5 before:inset-y-px before:inset-x-0',
childLabel: 'px-1.5 py-0.5',
childLink: 'p-1.5 gap-1.5 before:inset-y-px before:inset-x-0'
link: 'flex-row px-2.5 py-1.5 before:inset-y-px before:inset-x-0'
}
},
contentOrientation: {
@@ -80,13 +74,13 @@ export default (options: Required<ModuleOptions>) => ({
},
active: {
true: {
childLink: 'before:bg-elevated text-highlighted',
childLink: 'bg-elevated text-highlighted',
childLinkIcon: 'text-default'
},
false: {
link: 'text-muted',
linkLeadingIcon: 'text-dimmed',
childLink: ['hover:before:bg-elevated/50 text-default hover:text-highlighted', options.theme.transitions && 'transition-colors before:transition-colors'],
childLink: ['hover:bg-elevated/50 text-default hover:text-highlighted', options.theme.transitions && 'transition-colors'],
childLinkIcon: ['text-dimmed group-hover:text-default', options.theme.transitions && 'transition-colors']
}
},
@@ -118,21 +112,6 @@ export default (options: Required<ModuleOptions>) => ({
childList: 'gap-1',
content: 'w-60'
}
}, {
orientation: 'vertical',
collapsed: false,
class: {
childList: 'ms-5 border-s border-default',
childItem: 'ps-1.5 -ms-px',
content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden'
}
}, {
orientation: 'vertical',
collapsed: true,
class: {
link: 'px-1.5',
content: 'shadow-sm rounded-sm min-h-6 p-1'
}
}, {
orientation: 'horizontal',
highlight: true,
@@ -207,7 +186,6 @@ export default (options: Required<ModuleOptions>) => ({
variant: 'pill',
active: true,
highlight: true,
disabled: false,
class: {
link: ['hover:before:bg-elevated/50', options.theme.transitions && 'before:transition-colors']
}
@@ -260,6 +238,19 @@ export default (options: Required<ModuleOptions>) => ({
class: {
link: 'after:bg-inverted'
}
}, {
orientation: 'vertical',
collapsed: false,
class: {
childList: 'ms-5 border-s border-default',
childItem: 'ps-1.5 -ms-px'
}
}, {
orientation: 'vertical',
collapsed: true,
class: {
link: 'px-1.5'
}
}],
defaultVariants: {
color: 'primary',

Some files were not shown because too many files have changed in this diff Show More