refactor: no more vue multiselect (#5523)

* start multiselect component

* update styles

* small fix

* fix padding and styles

* add border bottom on sticky items

* add border bottom to search as well

* fix select all showing line

* use multi-select component for languages field

* add no options story for empty state

* refactor: remove vue-multiselect, replace with either our own combobox and multiselect

* pnpm prepr

* pnpm prepr

* fix combobox in transfer organization
This commit is contained in:
Truman Gao
2026-03-16 05:46:48 -07:00
committed by GitHub
parent d50a8efb26
commit 01c9dee612
15 changed files with 172 additions and 240 deletions

View File

@@ -40,7 +40,6 @@
"vite-svg-loader": "^5.1.0",
"vue": "^3.5.13",
"vue-i18n": "^10.0.0",
"vue-multiselect": "3.0.0",
"vue-router": "^4.6.0",
"vue-virtual-scroller": "v2.0.0-beta.8"
},

View File

@@ -1599,4 +1599,3 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
}
}
</style>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -17,31 +17,17 @@
<tr class="content">
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
<td>
<multiselect
<Combobox
v-if="versions?.length > 1"
v-model="selectedVersion"
:options="versions"
v-model="selectedVersionId"
:options="versionOptions"
:searchable="true"
placeholder="Select version"
open-direction="top"
:show-labels="false"
:custom-label="
(version) =>
`${version?.name} (${version?.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${version?.game_versions.join(', ')})`
"
force-direction="up"
:max-height="150"
/>
<span v-else>
<span>
{{ selectedVersion?.name }} ({{
selectedVersion?.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')
}}
- {{ selectedVersion?.game_versions.join(', ') }})
</span>
<span>{{ selectedVersionLabel }}</span>
</span>
</td>
</tr>
@@ -59,9 +45,8 @@
<script setup>
import { DownloadIcon, XIcon } from '@modrinth/assets'
import { Button, formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { ref } from 'vue'
import Multiselect from 'vue-multiselect'
import { Button, Combobox, formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { computed, ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
@@ -79,11 +64,35 @@ const installing = ref(false)
const onInstall = ref(() => {})
const selectedVersionLabel = computed(() => {
if (!selectedVersion.value) return ''
return `${selectedVersion.value.name} (${selectedVersion.value.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${selectedVersion.value.game_versions.join(', ')})`
})
const versionOptions = computed(() =>
(versions.value ?? []).map((version) => ({
value: version.id,
label: `${version.name} (${version.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${version.game_versions.join(', ')})`,
})),
)
const selectedVersionId = computed({
get: () => selectedVersion.value?.id ?? null,
set: (value) => {
if (!value) return
selectedVersion.value = (versions.value ?? []).find((version) => version.id === value) ?? null
},
})
defineExpose({
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
instance.value = instanceVal
versions.value = projectVersions
selectedVersion.value = selected ?? projectVersions[0]
versions.value = projectVersions ?? []
selectedVersion.value = selected ?? projectVersions?.[0] ?? null
project.value = projectVal
@@ -162,9 +171,5 @@ td:first-child {
display: flex;
flex-direction: column;
gap: 1rem;
:deep(.animated-dropdown .options) {
max-height: 13.375rem;
}
}
</style>

View File

@@ -75,7 +75,6 @@
"semver": "^7.5.4",
"three": "^0.172.0",
"vue-confetti-explosion": "^1.0.2",
"vue-multiselect": "3.0.0-alpha.2",
"vue-typed-virtual-list": "^1.0.10",
"vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.5.2",

View File

@@ -1593,4 +1593,3 @@ const { cycle: changeTheme } = useTheme()
}
}
</style>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -296,19 +296,14 @@
This project is not managed by an organization. If you are the member of any organizations,
you can transfer management to one of them.
</p>
<div v-if="!organization" class="input-group">
<Multiselect
id="organization-picker"
v-model="selectedOrganization"
class="large-multiselect"
track-by="id"
label="name"
open-direction="top"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
:options="organizations || []"
:disabled="!currentMember?.is_owner || organizations?.length === 0"
<div v-if="!organization" class="flex gap-2">
<Combobox
v-model="selectedOrganizationId"
:options="organizationOptions"
:searchable="true"
search-placeholder="Select organization..."
force-direction="up"
:disabled="!currentMember?.is_owner || organizationOptions.length === 0"
/>
<button
class="btn btn-primary"
@@ -316,7 +311,7 @@
@click="openTransferToOrgModal($event)"
>
<CheckIcon />
Transfer management
<span class="w-max"> Transfer management </span>
</button>
</div>
<button v-if="organization" class="btn" @click="$refs.modal_remove.show()">
@@ -561,13 +556,13 @@ import {
Badge,
Card,
Checkbox,
Combobox,
ConfirmModal,
injectNotificationManager,
injectProjectPageContext,
StyledInput,
Toggle,
} from '@modrinth/ui'
import { Multiselect } from 'vue-multiselect'
import ConfirmTransferProjectModal from '~/components/ui/ConfirmTransferProjectModal.vue'
import { removeSelfFromTeam } from '~/helpers/teams.js'
@@ -620,7 +615,7 @@ initMembers()
const currentUsername = ref('')
const openTeamMembers = ref([])
const selectedOrganization = ref(null)
const selectedOrganizationId = ref('')
const transferData = ref(null)
const transferModal = ref(null)
@@ -630,6 +625,17 @@ const { data: organizations } = useAsyncData('organizations', () => {
})
})
const organizationOptions = computed(() =>
(organizations.value ?? []).map((organization) => ({
value: organization.id,
label: organization.name,
})),
)
const selectedOrganization = computed(() =>
(organizations.value ?? []).find((org) => org.id === selectedOrganizationId.value),
)
const UPLOAD_VERSION = 1 << 0
const DELETE_VERSION = 1 << 1
const EDIT_DETAILS = 1 << 2
@@ -642,9 +648,9 @@ const VIEW_ANALYTICS = 1 << 8
const VIEW_PAYOUTS = 1 << 9
const onAddToOrg = useClientTry(async () => {
if (!selectedOrganization.value) return
if (!selectedOrganizationId.value) return
await useBaseFetch(`organization/${selectedOrganization.value.id}/projects`, {
await useBaseFetch(`organization/${selectedOrganizationId.value}/projects`, {
method: 'POST',
body: JSON.stringify({
project_id: project.value.id,
@@ -1002,8 +1008,4 @@ const updateMembers = async () => {
}
}
}
.large-multiselect {
max-width: 24rem;
}
</style>

View File

@@ -25,17 +25,14 @@
The mod loaders you would like to package your data pack for.
</span>
</label>
<multiselect
<MultiSelect
id="package-mod-loaders"
v-model="packageLoaders"
:options="['fabric', 'forge', 'quilt', 'neoforge']"
:custom-label="(value: string) => value.charAt(0).toUpperCase() + value.slice(1)"
:multiple="true"
class="package-loader-select"
:options="packageLoaderOptions"
:searchable="false"
:show-no-results="false"
:show-labels="false"
placeholder="Choose loaders..."
open-direction="top"
force-direction="up"
/>
<div class="button-group">
<ButtonStyled>
@@ -436,11 +433,11 @@ import {
ENVIRONMENTS_COPY,
injectNotificationManager,
injectProjectPageContext,
MultiSelect,
StyledInput,
useFormatDateTime,
} from '@modrinth/ui'
import { formatBytes, renderHighlightedString } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect'
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
@@ -504,6 +501,12 @@ const newFiles = ref<File[]>([])
const deleteFiles = ref<string[]>([])
const newFileTypes = ref<Array<{ display: string; value: string } | null>>([])
const packageLoaders = ref(['forge', 'fabric', 'quilt', 'neoforge'])
const packageLoaderOptions = [
{ value: 'fabric', label: 'Fabric' },
{ value: 'forge', label: 'Forge' },
{ value: 'quilt', label: 'Quilt' },
{ value: 'neoforge', label: 'Neoforge' },
]
const showKnownErrors = ref(false)
const shouldPreventActions = ref(false)
const uploadedImageIds = ref<string[]>([])
@@ -1215,11 +1218,6 @@ async function resetProjectVersions() {
margin-bottom: var(--spacing-card-sm);
}
.multiselect {
width: 8rem;
flex-grow: 1;
}
input {
flex-grow: 2;
}
@@ -1270,14 +1268,6 @@ async function resetProjectVersions() {
font-weight: 300;
}
.raised-multiselect {
display: none;
margin: 0 0.5rem;
height: 40px;
max-height: 40px;
min-width: 235px;
}
.raised-button {
margin-left: auto;
background-color: var(--color-raised-bg);
@@ -1286,13 +1276,6 @@ async function resetProjectVersions() {
&:not(:nth-child(2)) {
margin-top: 0.5rem;
}
// TODO: Make file type editing work on mobile
@media (min-width: 600px) {
.raised-multiselect {
display: block;
}
}
}
.additional-files {
@@ -1357,7 +1340,7 @@ async function resetProjectVersions() {
margin-bottom: 1rem;
}
.multiselect {
.package-loader-select {
max-width: 20rem;
}
}

View File

@@ -182,14 +182,11 @@
<div class="push-right">
<div class="labeled-control-row">
Sort by
<Multiselect
<Combobox
v-model="sortBy"
:searchable="false"
class="small-select"
:options="['Name', 'Status', 'Type']"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
:options="sortOptions"
@update:model-value="projects = updateSort(projects, sortBy, descending)"
/>
<button
@@ -323,6 +320,7 @@ import {
Avatar,
ButtonStyled,
Checkbox,
Combobox,
commonMessages,
CopyCode,
injectNotificationManager,
@@ -332,7 +330,6 @@ import {
useVIntl,
} from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import { getProjectTypeForUrl } from '~/helpers/projects.js'
@@ -356,6 +353,11 @@ const projects = ref([])
const projectsWithMigrationWarning = ref([])
const selectedProjects = ref([])
const sortBy = ref('Name')
const sortOptions = [
{ value: 'Name', label: 'Name' },
{ value: 'Status', label: 'Status' },
{ value: 'Type', label: 'Type' },
]
const descending = ref(false)
const editLinks = reactive({
showAffected: false,

View File

@@ -106,19 +106,22 @@
@input="updateSearchProjects"
/>
<div class="sort-by">
<span class="label">{{ formatMessage(commonMessages.sortByLabel) }}</span>
<Multiselect
<DropdownSelect
v-slot="{ selected }"
v-model="sortType"
placeholder="Select one"
class="selector"
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
:options="['relevance', 'downloads', 'follows', 'updated', 'newest']"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@update:model-value="updateSearchProjects"
/>
class="!h-9 !w-max flex-grow"
name="Sort by"
:options="sortOptions"
:display-name="(value) => value?.charAt(0).toUpperCase() + value?.slice(1)"
@change="updateSearchProjects()"
>
<div>
<span class="font-semibold text-primary"
>{{ formatMessage(commonMessages.sortByLabel) }}:
</span>
<span class="font-semibold text-secondary">{{ selected }}</span>
</div>
</DropdownSelect>
</div>
</div>
<div class="results display-mode--list">
@@ -444,6 +447,7 @@ import {
ButtonStyled,
commonMessages,
defineMessages,
DropdownSelect,
IntlFormatted,
ProjectCard,
StyledInput,
@@ -451,7 +455,6 @@ import {
useVIntl,
} from '@modrinth/ui'
import { ref } from 'vue'
import { Multiselect } from 'vue-multiselect'
import ATLauncherLogo from '~/assets/images/external/atlauncher.svg?component'
import PrismLauncherLogo from '~/assets/images/external/prism.svg?component'
@@ -464,6 +467,7 @@ const { formatMessage } = useVIntl()
const searchQuery = ref('leave')
const sortType = ref('relevance')
const sortOptions = ['relevance', 'downloads', 'follows', 'updated', 'newest']
const PROJECT_COUNT = 100000
@@ -955,7 +959,7 @@ const creatorFeatureMessages = defineMessages({
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
gap: 1rem;
gap: 0.5rem;
.iconified-input {
width: 100%;
@@ -977,19 +981,6 @@ const creatorFeatureMessages = defineMessages({
display: flex;
gap: 0.75rem;
align-items: center;
.label {
white-space: nowrap;
}
.selector {
min-width: 8rem;
white-space: nowrap;
}
@media screen and (max-width: 500px) {
display: none;
}
}
}

View File

@@ -186,17 +186,12 @@
<div class="push-right">
<div class="labeled-control-row">
Sort by
<Multiselect
<DropdownSelect
v-model="sortBy"
:searchable="false"
class="small-select"
:options="['Name', 'Status', 'Type']"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@update:model-value="
sortedProjects = updateSort(sortedProjects, sortBy, descending)
"
class="!w-auto"
name="Sort by"
:options="sortOptions"
@change="sortedProjects = updateSort(sortedProjects, sortBy, descending)"
/>
<button
v-tooltip="descending ? 'Descending' : 'Ascending'"
@@ -331,6 +326,7 @@ import {
Checkbox,
commonMessages,
CopyCode,
DropdownSelect,
injectNotificationManager,
NewModal,
ProjectStatusBadge,
@@ -338,7 +334,6 @@ import {
useVIntl,
} from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import OrganizationProjectTransferModal from '~/components/ui/OrganizationProjectTransferModal.vue'
@@ -466,6 +461,7 @@ const updateSort = (inputProjects, sort, descending) => {
const sortedProjects = ref(updateSort(projects.value, 'Name'))
const selectedProjects = ref([])
const sortBy = ref('Name')
const sortOptions = ['Name', 'Status', 'Type']
const descending = ref(false)
const editLinksModal = ref(null)
@@ -674,11 +670,6 @@ const onBulkEditLinks = async () => {
white-space: nowrap;
}
.small-select {
width: -moz-fit-content;
width: fit-content;
}
.label-button[data-active='true'] {
--background-color: var(--color-red);
--text-color: var(--color-brand-inverted);

View File

@@ -80,7 +80,6 @@
"qrcode.vue": "^3.4.1",
"three": "^0.172.0",
"vue-i18n": "^10.0.0",
"vue-multiselect": "3.0.0",
"vue-select": "4.0.0-beta.6",
"vue-typed-virtual-list": "^1.0.10",
"vue3-ace-editor": "^2.2.4",

View File

@@ -323,92 +323,15 @@
</div>
</div>
<p class="my-2 text-lg font-bold">Pay for it with</p>
<multiselect
v-model="selectedPaymentMethod"
<Combobox
v-model="selectedPaymentMethodId"
placeholder="Payment method"
label="id"
track-by="id"
:options="selectablePaymentMethods"
:option-height="104"
:show-labels="false"
:options="selectablePaymentMethodOptions"
:searchable="false"
:close-on-select="true"
:allow-empty="false"
open-direction="top"
:show-icon-in-selected="true"
force-direction="up"
class="max-w-[20rem]"
@select="selectPaymentMethod"
>
<!-- eslint-disable-next-line vue/no-template-shadow -->
<template #singleLabel="props">
<div class="flex items-center gap-2">
<CardIcon v-if="props.option.type === 'card'" class="h-8 w-8" />
<CurrencyIcon v-else-if="props.option.type === 'cashapp'" class="h-8 w-8" />
<PayPalIcon v-else-if="props.option.type === 'paypal'" class="h-8 w-8" />
<span v-if="props.option.type === 'card'">
{{
formatMessage(paymentMethodMessages.paymentMethodCardDisplay, {
card_brand:
formatMessage(paymentMethodMessages[props.option.card.brand]) ??
formatMessage(paymentMethodMessages.unknown),
last_four: props.option.card.last4,
})
}}
</span>
<template v-else>
{{
formatMessage(paymentMethodMessages[props.option.type]) ??
formatMessage(paymentMethodMessages.unknown)
}}
</template>
<span v-if="props.option.type === 'cashapp' && props.option.cashapp.cashtag">
({{ props.option.cashapp.cashtag }})
</span>
<span v-else-if="props.option.type === 'paypal' && props.option.paypal.payer_email">
({{ props.option.paypal.payer_email }})
</span>
</div>
</template>
<!-- eslint-disable-next-line vue/no-template-shadow -->
<template #option="props">
<div class="flex items-center gap-2">
<template v-if="props.option.id === 'new'">
<PlusIcon class="h-8 w-8" />
<span class="text-secondary">Add payment method</span>
</template>
<template v-else>
<CardIcon v-if="props.option.type === 'card'" class="h-8 w-8" />
<CurrencyIcon v-else-if="props.option.type === 'cashapp'" class="h-8 w-8" />
<PayPalIcon v-else-if="props.option.type === 'paypal'" class="h-8 w-8" />
<span v-if="props.option.type === 'card'">
{{
formatMessage(paymentMethodMessages.paymentMethodCardDisplay, {
card_brand:
formatMessage(paymentMethodMessages[props.option.card.brand]) ??
formatMessage(paymentMethodMessages.unknown),
last_four: props.option.card.last4,
})
}}
</span>
<template v-else>
{{
formatMessage(paymentMethodMessages[props.option.type]) ??
formatMessage(paymentMethodMessages.unknown)
}}
</template>
<span v-if="props.option.type === 'cashapp'">
({{ props.option.cashapp.cashtag }})
</span>
<span v-else-if="props.option.type === 'paypal'">
({{ props.option.paypal.payer_email }})
</span>
</template>
</div>
</template>
</multiselect>
/>
</div>
<p class="m-0 mt-9 text-sm text-secondary">
<strong>By clicking "Subscribe", you are purchasing a recurring subscription.</strong>
@@ -546,13 +469,13 @@ import {
import { calculateSavings, createStripeElements, getCurrency } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed, nextTick, reactive, ref, watch } from 'vue'
import { Multiselect } from 'vue-multiselect'
import { useVIntl } from '../../composables/i18n'
import { useFormatDateTime, useFormatPrice } from '../../composables/index.ts'
import { paymentMethodMessages } from '../../utils/common-messages'
import Admonition from '../base/Admonition.vue'
import Checkbox from '../base/Checkbox.vue'
import Combobox from '../base/Combobox.vue'
import Slider from '../base/Slider.vue'
import StyledInput from '../base/StyledInput.vue'
import AnimatedLogo from '../brand/AnimatedLogo.vue'
@@ -784,6 +707,67 @@ const selectablePaymentMethods = computed(() => {
return values
})
function formatPaymentMethodLabel(paymentMethod) {
if (!paymentMethod) {
return formatMessage(paymentMethodMessages.unknown)
}
if (paymentMethod.id === 'new') {
return 'Add payment method'
}
if (paymentMethod.type === 'card') {
return formatMessage(paymentMethodMessages.paymentMethodCardDisplay, {
card_brand:
formatMessage(paymentMethodMessages[paymentMethod.card?.brand]) ??
formatMessage(paymentMethodMessages.unknown),
last_four: paymentMethod.card?.last4 ?? '****',
})
}
const typeLabel =
formatMessage(paymentMethodMessages[paymentMethod.type]) ??
formatMessage(paymentMethodMessages.unknown)
let suffix = ''
if (paymentMethod.type === 'cashapp' && paymentMethod.cashapp?.cashtag) {
suffix = ` (${paymentMethod.cashapp.cashtag})`
} else if (paymentMethod.type === 'paypal' && paymentMethod.paypal?.payer_email) {
suffix = ` (${paymentMethod.paypal.payer_email})`
}
return `${typeLabel}${suffix}`
}
function getPaymentMethodIcon(paymentMethod) {
if (paymentMethod.id === 'new') return PlusIcon
if (paymentMethod.type === 'card') return CardIcon
if (paymentMethod.type === 'cashapp') return CurrencyIcon
if (paymentMethod.type === 'paypal') return PayPalIcon
return undefined
}
const selectablePaymentMethodOptions = computed(() =>
selectablePaymentMethods.value.map((paymentMethod) => ({
value: paymentMethod.id,
label: formatPaymentMethodLabel(paymentMethod),
icon: getPaymentMethodIcon(paymentMethod),
})),
)
const selectedPaymentMethodId = computed({
get: () => selectedPaymentMethod.value?.id ?? null,
set: (value) => {
if (!value) return
const paymentMethod = selectablePaymentMethods.value.find((method) => method.id === value)
if (paymentMethod) {
selectedPaymentMethod.value = paymentMethod
void selectPaymentMethod(paymentMethod)
}
},
})
const primaryPaymentMethodId = computed(() => {
if (
props.customer &&

View File

@@ -1002,7 +1002,7 @@
"defaultMessage": "Singleplayer"
},
"label.sort-by": {
"defaultMessage": "Sort by"
"defaultMessage": "Sort by: "
},
"label.success": {
"defaultMessage": "Success"

View File

@@ -295,7 +295,7 @@ export const commonMessages = defineMessages({
},
sortByLabel: {
id: 'label.sort-by',
defaultMessage: 'Sort by',
defaultMessage: 'Sort by: ',
},
stopButton: {
id: 'button.stop',

21
pnpm-lock.yaml generated
View File

@@ -143,9 +143,6 @@ importers:
vue-i18n:
specifier: ^10.0.0
version: 10.0.8(vue@3.5.27(typescript@5.9.3))
vue-multiselect:
specifier: 3.0.0
version: 3.0.0
vue-router:
specifier: ^4.6.0
version: 4.6.4(vue@3.5.27(typescript@5.9.3))
@@ -353,9 +350,6 @@ importers:
vue-confetti-explosion:
specifier: ^1.0.2
version: 1.0.2(vue@3.5.27(typescript@5.9.3))
vue-multiselect:
specifier: 3.0.0-alpha.2
version: 3.0.0-alpha.2
vue-typed-virtual-list:
specifier: ^1.0.10
version: 1.0.10(vue@3.5.27(typescript@5.9.3))
@@ -676,9 +670,6 @@ importers:
vue-i18n:
specifier: ^10.0.0
version: 10.0.8(vue@3.5.27(typescript@5.9.3))
vue-multiselect:
specifier: 3.0.0
version: 3.0.0
vue-select:
specifier: 4.0.0-beta.6
version: 4.0.0-beta.6(vue@3.5.27(typescript@5.9.3))
@@ -9467,14 +9458,6 @@ packages:
peerDependencies:
vue: '>=2'
vue-multiselect@3.0.0:
resolution: {integrity: sha512-uupKdINgz7j83lQToCL7KkgQQxvG43el++hsR39YT9pCe1DwzUGmKzPxjVP6rqskXed5P6DtUASYAlCliW740Q==}
engines: {node: '>= 14.18.1', npm: '>= 6.14.15'}
vue-multiselect@3.0.0-alpha.2:
resolution: {integrity: sha512-Xp9fGJECns45v+v8jXbCIsAkCybYkEg0lNwr7Z6HDUSMyx2TEIK2giipPE+qXiShEc1Ipn+ZtttH2iq9hwXP4Q==}
engines: {node: '>= 4.0.0', npm: '>= 3.0.0'}
vue-observe-visibility@2.0.0-alpha.1:
resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==}
peerDependencies:
@@ -19605,10 +19588,6 @@ snapshots:
dependencies:
vue: 3.5.27(typescript@5.9.3)
vue-multiselect@3.0.0: {}
vue-multiselect@3.0.0-alpha.2: {}
vue-observe-visibility@2.0.0-alpha.1(vue@3.5.27(typescript@5.9.3)):
dependencies:
vue: 3.5.27(typescript@5.9.3)