diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json
index d892df69f6..e44564ae98 100644
--- a/apps/app-frontend/package.json
+++ b/apps/app-frontend/package.json
@@ -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"
},
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index f7a029b5c3..c28a7b0d0e 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -1597,4 +1597,3 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
}
}
-
diff --git a/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue b/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue
index 7e8c6f7457..24f493ee03 100644
--- a/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue
+++ b/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue
@@ -17,31 +17,17 @@
| {{ instance?.loader }} {{ instance?.game_version }} |
-
-
- {{ selectedVersion?.name }} ({{
- selectedVersion?.loaders
- .map((name) => formatLoader(formatMessage, name))
- .join(', ')
- }}
- - {{ selectedVersion?.game_versions.join(', ') }})
-
+ {{ selectedVersionLabel }}
|
@@ -59,9 +45,8 @@
+
+
diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts
index 85251b0e15..2b3506366a 100644
--- a/packages/ui/src/components/base/index.ts
+++ b/packages/ui/src/components/base/index.ts
@@ -41,6 +41,8 @@ export { default as JoinedButtons } from './JoinedButtons.vue'
export { default as LoadingIndicator } from './LoadingIndicator.vue'
export { default as ManySelect } from './ManySelect.vue'
export { default as MarkdownEditor } from './MarkdownEditor.vue'
+export type { MultiSelectOption } from './MultiSelect.vue'
+export { default as MultiSelect } from './MultiSelect.vue'
export type { MaybeCtxFn, StageButtonConfig, StageConfigInput } from './MultiStageModal.vue'
export { default as MultiStageModal, resolveCtxFn } from './MultiStageModal.vue'
export { default as OptionGroup } from './OptionGroup.vue'
diff --git a/packages/ui/src/components/billing/PurchaseModal.vue b/packages/ui/src/components/billing/PurchaseModal.vue
index 95093b863d..d1753d10a0 100644
--- a/packages/ui/src/components/billing/PurchaseModal.vue
+++ b/packages/ui/src/components/billing/PurchaseModal.vue
@@ -323,92 +323,15 @@
Pay for it with
-
-
-
-
-
-
-
-
-
- {{
- formatMessage(paymentMethodMessages.paymentMethodCardDisplay, {
- card_brand:
- formatMessage(paymentMethodMessages[props.option.card.brand]) ??
- formatMessage(paymentMethodMessages.unknown),
- last_four: props.option.card.last4,
- })
- }}
-
-
- {{
- formatMessage(paymentMethodMessages[props.option.type]) ??
- formatMessage(paymentMethodMessages.unknown)
- }}
-
-
-
- ({{ props.option.cashapp.cashtag }})
-
-
- ({{ props.option.paypal.payer_email }})
-
-
-
-
-
-
-
-
- Add payment method
-
-
-
-
-
-
-
- {{
- formatMessage(paymentMethodMessages.paymentMethodCardDisplay, {
- card_brand:
- formatMessage(paymentMethodMessages[props.option.card.brand]) ??
- formatMessage(paymentMethodMessages.unknown),
- last_four: props.option.card.last4,
- })
- }}
-
-
- {{
- formatMessage(paymentMethodMessages[props.option.type]) ??
- formatMessage(paymentMethodMessages.unknown)
- }}
-
-
-
- ({{ props.option.cashapp.cashtag }})
-
-
- ({{ props.option.paypal.payer_email }})
-
-
-
-
-
+ />
By clicking "Subscribe", you are purchasing a recurring subscription.
@@ -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 &&
diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json
index 4200211520..73658b9390 100644
--- a/packages/ui/src/locales/en-US/index.json
+++ b/packages/ui/src/locales/en-US/index.json
@@ -978,7 +978,7 @@
"defaultMessage": "Singleplayer"
},
"label.sort-by": {
- "defaultMessage": "Sort by"
+ "defaultMessage": "Sort by: "
},
"label.success": {
"defaultMessage": "Success"
diff --git a/packages/ui/src/stories/base/MultiSelect.stories.ts b/packages/ui/src/stories/base/MultiSelect.stories.ts
new file mode 100644
index 0000000000..6d7b59f4b0
--- /dev/null
+++ b/packages/ui/src/stories/base/MultiSelect.stories.ts
@@ -0,0 +1,95 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+import { ref } from 'vue'
+
+import MultiSelect from '../../components/base/MultiSelect.vue'
+
+const meta = {
+ title: 'Base/MultiSelect',
+ // @ts-ignore - error comes from generically typed component
+ component: MultiSelect,
+ render: (args) => ({
+ components: { MultiSelect },
+ setup() {
+ const selected = ref(args.modelValue)
+ return { args, selected }
+ },
+ template: /*html*/ `
+
+
+
+ `,
+ }),
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ options: [
+ { value: 'en', label: 'English' },
+ { value: 'es', label: 'Spanish' },
+ { value: 'fr', label: 'French' },
+ { value: 'de', label: 'German' },
+ { value: 'zh-CN', label: 'Chinese (Simplified)' },
+ { value: 'ko', label: 'Korean' },
+ { value: 'ja', label: 'Japanese' },
+ { value: 'pt', label: 'Portuguese' },
+ { value: 'ru', label: 'Russian' },
+ { value: 'it', label: 'Italian' },
+ { value: 'ar', label: 'Arabic' },
+ ],
+ modelValue: ['en', 'es', 'fr', 'zh-CN'],
+ placeholder: 'Select languages',
+ },
+}
+
+export const WithSearch: Story = {
+ args: {
+ ...Default.args,
+ searchable: true,
+ searchPlaceholder: 'Search versions',
+ },
+}
+
+export const WithSelectAll: Story = {
+ args: {
+ ...Default.args,
+ searchable: true,
+ includeSelectAllOption: true,
+ searchPlaceholder: 'Search versions',
+ },
+}
+
+export const ManySelected: Story = {
+ args: {
+ ...Default.args,
+ modelValue: ['en', 'es', 'fr', 'zh-CN', 'ko', 'ja', 'pt', 'ru', 'it', 'ar', 'de'],
+ searchable: true,
+ includeSelectAllOption: true,
+ },
+}
+
+export const TwoTagRows: Story = {
+ args: {
+ ...ManySelected.args,
+ maxTagRows: 2,
+ },
+}
+
+export const NoOptions: Story = {
+ args: {
+ ...Default.args,
+ options: [],
+ modelValue: [],
+ searchable: true,
+ noOptionsMessage: 'No options available',
+ },
+}
+
+export const Empty: Story = {
+ args: {
+ ...Default.args,
+ modelValue: [],
+ },
+}
diff --git a/packages/ui/src/utils/common-messages.ts b/packages/ui/src/utils/common-messages.ts
index a4d052db1c..a98c3275ef 100644
--- a/packages/ui/src/utils/common-messages.ts
+++ b/packages/ui/src/utils/common-messages.ts
@@ -291,7 +291,7 @@ export const commonMessages = defineMessages({
},
sortByLabel: {
id: 'label.sort-by',
- defaultMessage: 'Sort by',
+ defaultMessage: 'Sort by: ',
},
stopButton: {
id: 'button.stop',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7a10e78768..887a2cc10e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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)