diff --git a/apps/frontend/src/pages/[type]/[id]/settings/server.vue b/apps/frontend/src/pages/[type]/[id]/settings/server.vue
index af88816222..cac014ed6f 100644
--- a/apps/frontend/src/pages/[type]/[id]/settings/server.vue
+++ b/apps/frontend/src/pages/[type]/[id]/settings/server.vue
@@ -26,15 +26,13 @@
>Languages (optional)
-
@@ -166,10 +164,10 @@ import {
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
+ MultiSelect,
StyledInput,
UnsavedChangesPopup,
} from '@modrinth/ui'
-import { Multiselect } from 'vue-multiselect'
import CompatibilityCard from '~/components/ui/project-settings/CompatibilityCard.vue'
diff --git a/packages/assets/styles/defaults.scss b/packages/assets/styles/defaults.scss
index d77191e349..6b6a3d59ce 100644
--- a/packages/assets/styles/defaults.scss
+++ b/packages/assets/styles/defaults.scss
@@ -158,11 +158,11 @@ h3 {
}
::-webkit-scrollbar-thumb {
- background: var(--color-button-bg);
+ background: var(--color-scrollbar);
}
// Firefox scrollbar
* {
scrollbar-width: thin;
- scrollbar-color: var(--color-button-bg) transparent;
+ scrollbar-color: var(--color-scrollbar) transparent;
}
diff --git a/packages/assets/styles/variables.scss b/packages/assets/styles/variables.scss
index 76be6f6da8..7106722110 100644
--- a/packages/assets/styles/variables.scss
+++ b/packages/assets/styles/variables.scss
@@ -320,7 +320,7 @@ html {
--color-button-bg: var(--surface-4);
--color-button-border: rgba(193, 190, 209, 0.12);
- --color-scrollbar: var(--color-button-bg);
+ --color-scrollbar: var(--surface-5);
--color-divider: var(--color-button-bg);
--color-divider-dark: #646c75;
diff --git a/packages/ui/src/components/base/MultiSelect.vue b/packages/ui/src/components/base/MultiSelect.vue
new file mode 100644
index 0000000000..f8b76e6ac9
--- /dev/null
+++ b/packages/ui/src/components/base/MultiSelect.vue
@@ -0,0 +1,711 @@
+
+
+
+
+
+ {{ tag.label }}
+
+
+
+
+ {{ placeholder }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectAllLabel }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+ {{ noOptionsMessage }}
+
+
+ {{ noResultsMessage }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts
index 48aeb49577..ec4805053d 100644
--- a/packages/ui/src/components/base/index.ts
+++ b/packages/ui/src/components/base/index.ts
@@ -39,6 +39,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/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: [],
+ },
+}