feat: add mobile-responsive drawer for search & filters

Replaces desktop-only popover components with responsive alternatives:
- Adds drawer component for mobile view using vaul-vue
- Extracts shared templates for reusability
- Adjusts layout and height based on viewport size
- Maintains desktop popover behavior on larger screens

Improves mobile user experience while preserving desktop functionality
This commit is contained in:
ccbikai 2025-03-02 15:56:20 +08:00
parent 6070f2f5c3
commit 67c588eb12
12 changed files with 361 additions and 73 deletions

View file

@ -1,11 +1,16 @@
<script setup>
import { useUrlSearchParams, watchDebounced } from '@vueuse/core'
import { createReusableTemplate, useMediaQuery, useUrlSearchParams, watchDebounced } from '@vueuse/core'
import { safeDestr } from 'destr'
import { Check, ChevronsUpDown } from 'lucide-vue-next'
import { VList } from 'virtua/vue'
const emit = defineEmits(['change'])
const [TriggerTemplate, TriggerComponent] = createReusableTemplate()
const [FilterTemplate, FilterComponent] = createReusableTemplate()
const isDesktop = useMediaQuery('(min-width: 640px)')
const links = ref([])
const isOpen = ref(false)
const selectedLinks = ref([])
@ -38,46 +43,61 @@ onBeforeMount(() => {
</script>
<template>
<Popover v-model:open="isOpen">
<TriggerTemplate>
<Button
variant="outline"
role="combobox"
:aria-expanded="isOpen"
class="flex justify-between w-full sm:w-48"
>
<div class="flex-1 font-normal text-left truncate" :class="selectedLinks.length ? 'text-foreground' : 'text-muted-foreground'">
{{ selectedLinks.length ? selectedLinks.join(', ') : 'Filter Links...' }}
</div>
<ChevronsUpDown class="ml-2 w-4 h-4 opacity-50 shrink-0" />
</Button>
</TriggerTemplate>
<FilterTemplate>
<Command v-model="selectedLinks" multiple>
<CommandInput :placeholder="selectedLinks.length ? selectedLinks.join(', ') : 'Filter Links...'" />
<CommandEmpty>No link found.</CommandEmpty>
<CommandList :class="{ 'max-h-none': !isDesktop }">
<CommandGroup>
<VList
v-slot="{ item: link }"
:data="links"
:style="{ height: isDesktop ? '292px' : '420px' }"
>
<CommandItem
:value="link.slug"
>
<Check
:class="cn(
'mr-2 h-4 w-4',
selectedLinks.includes(link.slug) ? 'opacity-100' : 'opacity-0',
)"
/>
{{ link.slug }}
</CommandItem>
</VList>
</CommandGroup>
</CommandList>
</Command>
</FilterTemplate>
<Popover v-if="isDesktop" v-model:open="isOpen">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="isOpen"
class="flex justify-between w-full sm:w-48"
>
<div class="flex-1 font-normal text-left truncate" :class="selectedLinks.length ? 'text-foreground' : 'text-muted-foreground'">
{{ selectedLinks.length ? selectedLinks.join(', ') : 'Filter Links...' }}
</div>
<ChevronsUpDown class="ml-2 w-4 h-4 opacity-50 shrink-0" />
</Button>
<TriggerComponent />
</PopoverTrigger>
<PopoverContent class="p-0 w-full sm:w-48">
<Command v-model="selectedLinks" multiple>
<CommandInput placeholder="Filter Links..." />
<CommandEmpty>No link found.</CommandEmpty>
<CommandList>
<CommandGroup>
<VList
v-slot="{ item: link }"
:data="links"
:style="{ height: '292px' }"
>
<CommandItem
:value="link.slug"
>
<Check
:class="cn(
'mr-2 h-4 w-4',
selectedLinks.includes(link.slug) ? 'opacity-100' : 'opacity-0',
)"
/>
{{ link.slug }}
</CommandItem>
</VList>
</CommandGroup>
</CommandList>
</Command>
<FilterComponent />
</PopoverContent>
</Popover>
<Drawer v-else v-model:open="isOpen">
<DrawerTrigger as-child>
<TriggerComponent />
</DrawerTrigger>
<DrawerContent class="h-[500px]">
<FilterComponent />
</DrawerContent>
</Drawer>
</template>

View file

@ -1,7 +1,12 @@
<script setup>
import { useMagicKeys } from '@vueuse/core'
import { createReusableTemplate, useMagicKeys, useMediaQuery } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse'
const [TriggerTemplate, TriggerComponent] = createReusableTemplate()
const [SearchTemplate, SearchComponent] = createReusableTemplate()
const isDesktop = useMediaQuery('(min-width: 640px)')
const router = useRouter()
const isOpen = ref(false)
@ -48,12 +53,11 @@ onMounted(() => {
</script>
<template>
<div>
<TriggerTemplate>
<Button
variant="outline"
size="sm"
class="relative h-10 w-full justify-start bg-background text-muted-foreground sm:w-32 md:w-48"
@click="isOpen = true"
class="relative justify-start w-full h-10 bg-background text-muted-foreground sm:w-32 md:w-48"
>
<span class="hidden md:inline-flex">Search Links...</span>
<span class="inline-flex md:hidden">Search</span>
@ -61,36 +65,50 @@ onMounted(() => {
<span class="text-xs"></span>K
</kbd>
</Button>
<Dialog :open="isOpen" @update:open="isOpen = !isOpen">
<DialogContent class="overflow-hidden p-0 shadow-lg">
<Command v-model:search-term="searchTerm" v-model="selectedLink" class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<CommandInput placeholder="Type to search..." />
<CommandList>
<CommandEmpty v-if="searchTerm">
No links found.
</CommandEmpty>
<CommandGroup heading="Links">
<CommandItem v-for="link in filteredLinks" :key="link.item?.id" class="cursor-pointer" :value="link.item" @select="selectLink(link.item)">
<div class="flex gap-1 w-full">
<div class="flex-1 overflow-hidden inline-flex gap-1 items-center">
<div class="text-sm font-medium">
{{ link.item?.slug }}
</div>
<div class="text-xs text-muted-foreground flex-1 truncate">
({{ link.item?.url }})
</div>
</div>
<Badge v-if="link.item?.comment" variant="secondary">
<div class="max-w-24 truncate">
{{ link.item?.comment }}
</div>
</Badge>
</TriggerTemplate>
<SearchTemplate>
<Command v-model:search-term="searchTerm" v-model="selectedLink" class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<CommandInput placeholder="Type to search..." />
<CommandList :class="{ 'max-h-none': !isDesktop }">
<CommandEmpty v-if="searchTerm">
No links found.
</CommandEmpty>
<CommandGroup heading="Links">
<CommandItem v-for="link in filteredLinks" :key="link.item?.id" class="cursor-pointer" :value="link.item" @select="selectLink(link.item)">
<div class="flex gap-1 w-full">
<div class="inline-flex overflow-hidden flex-1 gap-1 items-center">
<div class="text-sm font-medium">
{{ link.item?.slug }}
</div>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</DialogContent>
</Dialog>
</div>
<div class="flex-1 text-xs truncate text-muted-foreground">
({{ link.item?.url }})
</div>
</div>
<Badge v-if="link.item?.comment" variant="secondary">
<div class="truncate max-w-24">
{{ link.item?.comment }}
</div>
</Badge>
</div>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</SearchTemplate>
<Dialog v-if="isDesktop" v-model:open="isOpen">
<DialogTrigger as-child>
<TriggerComponent />
</DialogTrigger>
<DialogContent class="overflow-hidden p-0 shadow-lg">
<SearchComponent />
</DialogContent>
</Dialog>
<Drawer v-else v-model:open="isOpen">
<DrawerTrigger as-child>
<TriggerComponent />
</DrawerTrigger>
<DrawerContent class="h-[500px]">
<SearchComponent />
</DrawerContent>
</Drawer>
</template>

View file

@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { DrawerRootEmits, DrawerRootProps } from 'vaul-vue'
import { useForwardPropsEmits } from 'radix-vue'
import { DrawerRoot } from 'vaul-vue'
const props = withDefaults(defineProps<DrawerRootProps>(), {
shouldScaleBackground: true,
})
const emits = defineEmits<DrawerRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerRoot v-bind="forwarded">
<slot />
</DrawerRoot>
</template>

View file

@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { DialogContentEmits, DialogContentProps } from 'radix-vue'
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useForwardPropsEmits } from 'radix-vue'
import { DrawerContent, DrawerPortal } from 'vaul-vue'
import DrawerOverlay from './DrawerOverlay.vue'
const props = defineProps<DialogContentProps & { class?: HtmlHTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerPortal>
<DrawerOverlay />
<DrawerContent
v-bind="forwarded" :class="cn(
'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
props.class,
)"
>
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
<slot />
</DrawerContent>
</DrawerPortal>
</template>

View file

@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { DrawerDescriptionProps } from 'vaul-vue'
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { DrawerDescription } from 'vaul-vue'
import { computed } from 'vue'
const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerDescription v-bind="delegatedProps" :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</DrawerDescription>
</template>

View file

@ -0,0 +1,14 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('grid gap-1.5 p-4 text-center sm:text-left', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { DialogOverlayProps } from 'radix-vue'
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { DrawerOverlay } from 'vaul-vue'
import { computed } from 'vue'
const props = defineProps<DialogOverlayProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerOverlay v-bind="delegatedProps" :class="cn('fixed inset-0 z-50 bg-black/80', props.class)" />
</template>

View file

@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { DrawerTitleProps } from 'vaul-vue'
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { DrawerTitle } from 'vaul-vue'
import { computed } from 'vue'
const props = defineProps<DrawerTitleProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerTitle v-bind="delegatedProps" :class="cn('text-lg font-semibold leading-none tracking-tight', props.class)">
<slot />
</DrawerTitle>
</template>

View file

@ -0,0 +1,8 @@
export { default as Drawer } from './Drawer.vue'
export { default as DrawerContent } from './DrawerContent.vue'
export { default as DrawerDescription } from './DrawerDescription.vue'
export { default as DrawerFooter } from './DrawerFooter.vue'
export { default as DrawerHeader } from './DrawerHeader.vue'
export { default as DrawerOverlay } from './DrawerOverlay.vue'
export { default as DrawerTitle } from './DrawerTitle.vue'
export { DrawerClose, DrawerPortal, DrawerTrigger } from 'vaul-vue'

View file

@ -35,6 +35,7 @@
"qr-code-styling": "1.6.0-rc.1",
"radix-vue": "^1.9.11",
"ua-parser-js": "next",
"vaul-vue": "^0.3.0",
"vee-validate": "^4.15.0",
"virtua": "^0.39.2",
"vue-sonner": "^1.3.0",

View file

@ -50,6 +50,9 @@ importers:
ua-parser-js:
specifier: next
version: 2.0.0-beta.3
vaul-vue:
specifier: ^0.3.0
version: 0.3.0(reka-ui@2.0.2(typescript@5.7.2)(vue@3.4.38(typescript@5.7.2)))(vue@3.4.38(typescript@5.7.2))
vee-validate:
specifier: ^4.15.0
version: 4.15.0(vue@3.4.38(typescript@5.7.2))
@ -1357,15 +1360,24 @@ packages:
'@floating-ui/core@1.6.1':
resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==}
'@floating-ui/dom@1.6.13':
resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
'@floating-ui/dom@1.6.7':
resolution: {integrity: sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==}
'@floating-ui/utils@0.2.4':
resolution: {integrity: sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==}
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@floating-ui/vue@1.1.1':
resolution: {integrity: sha512-cyawjk9etPZPl/RVtMRnWrwtAhWbPVSrRVYARgOzhLIqxr0k2up1APrrFjqP9QwRQ0AwjKSvbWg4YC6jESutow==}
'@floating-ui/vue@1.1.6':
resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -1993,9 +2005,17 @@ packages:
'@swc/helpers@0.5.11':
resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==}
'@tanstack/virtual-core@3.13.2':
resolution: {integrity: sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==}
'@tanstack/virtual-core@3.8.1':
resolution: {integrity: sha512-uNtAwenT276M9QYCjTBoHZ8X3MUeCRoGK59zPi92hMIxdfS9AyHjkDWJ94WroDxnv48UE+hIeo21BU84jKc8aQ==}
'@tanstack/vue-virtual@3.13.2':
resolution: {integrity: sha512-z4swzjdhzCh95n9dw9lTvw+t3iwSkYRlVkYkra3C9mul/m5fTzHR7KmtkwH4qXMTXGJUbngtC/bz2cHQIHkO8g==}
peerDependencies:
vue: ^2.7.0 || ^3.0.0
'@tanstack/vue-virtual@3.8.1':
resolution: {integrity: sha512-uhty1LzUbbcVc5zdMMSUjUt/ECTlMCtK49Ww7dH2m4lNNLGYwkj5SbfrAD8uCZxV1VeV7DRMXqhwUTELyR5rrA==}
peerDependencies:
@ -2494,6 +2514,9 @@ packages:
'@vueuse/core@12.2.0':
resolution: {integrity: sha512-jksyNu+5EGwggNkRWd6xX+8qBkYbmrwdFQMgCABsz+wq8bKF6w3soPFLB8vocFp3wFIzn0OYkSPM9JP+AFKwsg==}
'@vueuse/core@12.7.0':
resolution: {integrity: sha512-jtK5B7YjZXmkGNHjviyGO4s3ZtEhbzSgrbX+s5o+Lr8i2nYqNyHuPVOeTdM1/hZ5Tkxg/KktAuAVDDiHMraMVA==}
'@vueuse/integrations@12.2.0':
resolution: {integrity: sha512-Bc0unXiGNZ0w7xqSvzCuP7AflBRKcZX6ib7tGi7vAjOxhLd6GtN3J8qizIbSPnI62XyPy5fauOkq2i2HUPq6MQ==}
peerDependencies:
@ -2541,12 +2564,18 @@ packages:
'@vueuse/metadata@12.2.0':
resolution: {integrity: sha512-x6zynZtTh1l52m0y8d/EgzpshnMjg8cNZ2KWoncJ62Z5qPSGoc4FUunmMVrrRM/I/5542rTEY89CGftngZvrkQ==}
'@vueuse/metadata@12.7.0':
resolution: {integrity: sha512-4VvTH9mrjXqFN5LYa5YfqHVRI6j7R00Vy4995Rw7PQxyCL3z0Lli86iN4UemWqixxEvYfRjG+hF9wL8oLOn+3g==}
'@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
'@vueuse/shared@12.2.0':
resolution: {integrity: sha512-SRr4AZwv/giS+EmyA1ZIzn3/iALjjnWAGaBNmoDTMEob9JwQaevAocuaMDnPAvU7Z35Y5g3CFRusCWgp1gVJ3Q==}
'@vueuse/shared@12.7.0':
resolution: {integrity: sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw==}
abbrev@1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
@ -5510,6 +5539,11 @@ packages:
resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==}
hasBin: true
reka-ui@2.0.2:
resolution: {integrity: sha512-pC2UF6Z+kJF96aJvIErhkSO4DJYIeq9pgvh3pntNqcZb3zFGMzw8h2uny+GnLX2CKiQV54kZNYXxecYIiPMGyg==}
peerDependencies:
vue: '>= 3.2.0'
replace-in-file@6.3.5:
resolution: {integrity: sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==}
engines: {node: '>=10'}
@ -6244,6 +6278,12 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
vaul-vue@0.3.0:
resolution: {integrity: sha512-LwhBd7xLJtbdhq+vnQk9etOjZxr09GNFtMjOEYh969y5f8JKV5oR30CfB4toPkbtVMxFAFhMrp77EVWQgbtpHA==}
peerDependencies:
reka-ui: ^2.0.0
vue: ^3.3.0
vee-validate@4.15.0:
resolution: {integrity: sha512-PGJh1QCFwCBjbHu5aN6vB8macYVWrajbDvgo1Y/8fz9n/RVIkLmZCJDpUgu7+mUmCOPMxeyq7vXUOhbwAqdXcA==}
peerDependencies:
@ -7622,6 +7662,11 @@ snapshots:
dependencies:
'@floating-ui/utils': 0.2.4
'@floating-ui/dom@1.6.13':
dependencies:
'@floating-ui/core': 1.6.1
'@floating-ui/utils': 0.2.9
'@floating-ui/dom@1.6.7':
dependencies:
'@floating-ui/core': 1.6.1
@ -7629,6 +7674,8 @@ snapshots:
'@floating-ui/utils@0.2.4': {}
'@floating-ui/utils@0.2.9': {}
'@floating-ui/vue@1.1.1(vue@3.4.38(typescript@5.7.2))':
dependencies:
'@floating-ui/dom': 1.6.7
@ -7638,6 +7685,15 @@ snapshots:
- '@vue/composition-api'
- vue
'@floating-ui/vue@1.1.6(vue@3.4.38(typescript@5.7.2))':
dependencies:
'@floating-ui/dom': 1.6.13
'@floating-ui/utils': 0.2.9
vue-demi: 0.14.10(vue@3.4.38(typescript@5.7.2))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@ -8500,8 +8556,15 @@ snapshots:
dependencies:
tslib: 2.6.3
'@tanstack/virtual-core@3.13.2': {}
'@tanstack/virtual-core@3.8.1': {}
'@tanstack/vue-virtual@3.13.2(vue@3.4.38(typescript@5.7.2))':
dependencies:
'@tanstack/virtual-core': 3.13.2
vue: 3.4.38(typescript@5.7.2)
'@tanstack/vue-virtual@3.8.1(vue@3.4.38(typescript@5.7.2))':
dependencies:
'@tanstack/virtual-core': 3.8.1
@ -9243,6 +9306,15 @@ snapshots:
transitivePeerDependencies:
- typescript
'@vueuse/core@12.7.0(typescript@5.7.2)':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 12.7.0
'@vueuse/shared': 12.7.0(typescript@5.7.2)
vue: 3.5.13(typescript@5.7.2)
transitivePeerDependencies:
- typescript
'@vueuse/integrations@12.2.0(fuse.js@7.0.0)(typescript@5.7.2)':
dependencies:
'@vueuse/core': 12.2.0(typescript@5.7.2)
@ -9257,6 +9329,8 @@ snapshots:
'@vueuse/metadata@12.2.0': {}
'@vueuse/metadata@12.7.0': {}
'@vueuse/shared@10.11.1(vue@3.4.38(typescript@5.7.2))':
dependencies:
vue-demi: 0.14.10(vue@3.4.38(typescript@5.7.2))
@ -9270,6 +9344,12 @@ snapshots:
transitivePeerDependencies:
- typescript
'@vueuse/shared@12.7.0(typescript@5.7.2)':
dependencies:
vue: 3.5.13(typescript@5.7.2)
transitivePeerDependencies:
- typescript
abbrev@1.1.1: {}
abort-controller@3.0.0:
@ -12817,6 +12897,23 @@ snapshots:
dependencies:
jsesc: 0.5.0
reka-ui@2.0.2(typescript@5.7.2)(vue@3.4.38(typescript@5.7.2)):
dependencies:
'@floating-ui/dom': 1.6.13
'@floating-ui/vue': 1.1.6(vue@3.4.38(typescript@5.7.2))
'@internationalized/date': 3.5.4
'@internationalized/number': 3.5.3
'@tanstack/vue-virtual': 3.13.2(vue@3.4.38(typescript@5.7.2))
'@vueuse/core': 12.7.0(typescript@5.7.2)
'@vueuse/shared': 12.7.0(typescript@5.7.2)
aria-hidden: 1.2.4
defu: 6.1.4
ohash: 1.1.4
vue: 3.4.38(typescript@5.7.2)
transitivePeerDependencies:
- '@vue/composition-api'
- typescript
replace-in-file@6.3.5:
dependencies:
chalk: 4.1.2
@ -13643,6 +13740,14 @@ snapshots:
vary@1.1.2: {}
vaul-vue@0.3.0(reka-ui@2.0.2(typescript@5.7.2)(vue@3.4.38(typescript@5.7.2)))(vue@3.4.38(typescript@5.7.2)):
dependencies:
'@vueuse/core': 10.11.1(vue@3.4.38(typescript@5.7.2))
reka-ui: 2.0.2(typescript@5.7.2)(vue@3.4.38(typescript@5.7.2))
vue: 3.4.38(typescript@5.7.2)
transitivePeerDependencies:
- '@vue/composition-api'
vee-validate@4.15.0(vue@3.4.38(typescript@5.7.2)):
dependencies:
'@vue/devtools-api': 7.6.8