From 3c8bb0923d67288e46a62c83bb77182c576b1145 Mon Sep 17 00:00:00 2001
From: Prospector <6166773+Prospector@users.noreply.github.com>
Date: Tue, 28 Jul 2026 05:16:05 -0700
Subject: [PATCH 001/145] fix authorized apps settings, redesign, fix retro
theme vars (#6900)
* fix authorized apps settings, redesign, fix retro theme vars
* merge
* fix: retro in app
---------
Co-authored-by: Calum H. (IMB11)
---
.../ui/settings/AppearanceSettings.vue | 9 +-
apps/app-frontend/src/helpers/types.d.ts | 2 +-
apps/app-frontend/src/store/theme.ts | 2 +-
apps/frontend/src/assets/styles/global.scss | 10 +-
.../components/ui/AdsConsentNotification.vue | 1 +
.../src/components/ui/AuthorizationCard.vue | 165 +++++++++++
apps/frontend/src/locales/cs-CZ/index.json | 3 -
apps/frontend/src/locales/da-DK/index.json | 3 -
apps/frontend/src/locales/de-CH/index.json | 6 -
apps/frontend/src/locales/de-DE/index.json | 6 -
apps/frontend/src/locales/en-US/index.json | 17 +-
apps/frontend/src/locales/es-419/index.json | 6 -
apps/frontend/src/locales/es-ES/index.json | 6 -
apps/frontend/src/locales/fil-PH/index.json | 6 -
apps/frontend/src/locales/fr-FR/index.json | 6 -
apps/frontend/src/locales/hu-HU/index.json | 6 -
apps/frontend/src/locales/it-IT/index.json | 6 -
apps/frontend/src/locales/ja-JP/index.json | 6 -
apps/frontend/src/locales/ko-KR/index.json | 6 -
apps/frontend/src/locales/ms-MY/index.json | 6 -
apps/frontend/src/locales/nl-NL/index.json | 6 -
apps/frontend/src/locales/no-NO/index.json | 6 -
apps/frontend/src/locales/pl-PL/index.json | 6 -
apps/frontend/src/locales/pt-BR/index.json | 6 -
apps/frontend/src/locales/pt-PT/index.json | 6 -
apps/frontend/src/locales/ru-RU/index.json | 6 -
apps/frontend/src/locales/sr-CS/index.json | 3 -
apps/frontend/src/locales/sv-SE/index.json | 3 -
apps/frontend/src/locales/tr-TR/index.json | 6 -
apps/frontend/src/locales/uk-UA/index.json | 6 -
apps/frontend/src/locales/vi-VN/index.json | 6 -
apps/frontend/src/locales/zh-CN/index.json | 6 -
apps/frontend/src/locales/zh-TW/index.json | 6 -
apps/frontend/src/pages/settings.vue | 29 +-
.../src/pages/settings/authorizations.vue | 265 +++++++-----------
.../src/modules/labrinth/oauth/internal.ts | 17 +-
packages/app-lib/src/state/settings.rs | 3 +
packages/assets/styles/variables.scss | 33 +++
.../layouts/shared/user-profile/layout.vue | 10 +-
packages/ui/src/locales/en-US/index.json | 3 +
packages/ui/src/utils/common-messages.ts | 4 +
packages/utils/users.ts | 14 +
42 files changed, 383 insertions(+), 345 deletions(-)
create mode 100644 apps/frontend/src/components/ui/AuthorizationCard.vue
diff --git a/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue b/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue
index b986d25bc9..8100571a9a 100644
--- a/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue
+++ b/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue
@@ -1,6 +1,6 @@
+
diff --git a/packages/api-client/src/modules/labrinth/oauth/internal.ts b/packages/api-client/src/modules/labrinth/oauth/internal.ts
index 0b44186320..680009e05e 100644
--- a/packages/api-client/src/modules/labrinth/oauth/internal.ts
+++ b/packages/api-client/src/modules/labrinth/oauth/internal.ts
@@ -45,14 +45,15 @@ export class LabrinthOAuthInternalModule extends AbstractModule {
* @returns Promise resolving to an array of OAuth clients
*/
public async getApps(ids: string[]): Promise {
- return this.client.request(
- `/oauth/apps?ids=${encodeURIComponent(JSON.stringify(ids))}`,
- {
- api: 'labrinth',
- version: 'internal',
- method: 'GET',
- },
- )
+ if (ids.length === 0) {
+ return []
+ }
+
+ // bulk `/oauth/apps` is broken on backend, fetch by id instead
+ // TODO: Remove this once the backend is fixed
+ const results = await Promise.all(ids.map((id) => this.getApp(id).catch(() => null)))
+
+ return results.filter((app): app is Labrinth.OAuth.Internal.OAuthClient => app !== null)
}
/**
diff --git a/packages/app-lib/src/state/settings.rs b/packages/app-lib/src/state/settings.rs
index cd7d4615f5..011778996e 100644
--- a/packages/app-lib/src/state/settings.rs
+++ b/packages/app-lib/src/state/settings.rs
@@ -329,6 +329,7 @@ pub enum Theme {
Dark,
Light,
Oled,
+ Retro,
System,
}
@@ -338,6 +339,7 @@ impl Theme {
Theme::Dark => "dark",
Theme::Light => "light",
Theme::Oled => "oled",
+ Theme::Retro => "retro",
Theme::System => "system",
}
}
@@ -347,6 +349,7 @@ impl Theme {
"dark" => Theme::Dark,
"light" => Theme::Light,
"oled" => Theme::Oled,
+ "retro" => Theme::Retro,
"system" => Theme::System,
_ => Theme::Dark,
}
diff --git a/packages/assets/styles/variables.scss b/packages/assets/styles/variables.scss
index fdcb7bbabd..43bc28aa50 100644
--- a/packages/assets/styles/variables.scss
+++ b/packages/assets/styles/variables.scss
@@ -422,6 +422,39 @@ html {
}
.retro-mode {
+ @extend .dark-mode;
+ --surface-1: #191917;
+ --surface-2: rgb(22, 22, 21);
--surface-2-5: #3a3c3e;
+ --surface-3: #232421;
+ --surface-4: #3a3b38;
+ --surface-5: #5a5c58;
+ --color-button-bg: #3a3b38;
+ --color-base: #c3c4b3;
+ --color-secondary: #9b9e98;
+ --color-contrast: #e6e2d1;
+
+ --color-brand: #4d9227;
+ --color-brand-highlight: #25421e;
+ --color-accent-contrast: #ffffff;
+ --color-ad: var(--color-brand-highlight);
+ --color-ad-raised: var(--color-brand);
+ --color-ad-contrast: black;
+ --color-ad-highlight: var(--color-brand);
+
+ --color-red: rgb(232, 32, 13);
+ --color-orange: rgb(232, 141, 13);
+ --color-green: rgb(60, 219, 54);
+ --color-blue: rgb(9, 159, 239);
+ --color-purple: rgb(139, 129, 230);
+ --color-gray: #718096;
+
+ --color-red-highlight: rgba(232, 32, 13, 0.25);
+ --color-orange-highlight: rgba(232, 141, 13, 0.25);
+ --color-green-highlight: rgba(60, 219, 54, 0.25);
+ --color-blue-highlight: rgba(9, 159, 239, 0.25);
+ --color-purple-highlight: rgba(139, 129, 230, 0.25);
+ --color-gray-highlight: rgba(113, 128, 150, 0.25);
+
--brand-gradient-strong-bg: #3a3b38;
}
diff --git a/packages/ui/src/layouts/shared/user-profile/layout.vue b/packages/ui/src/layouts/shared/user-profile/layout.vue
index 28dcdf0f8d..6ba0cdf85d 100644
--- a/packages/ui/src/layouts/shared/user-profile/layout.vue
+++ b/packages/ui/src/layouts/shared/user-profile/layout.vue
@@ -402,7 +402,11 @@ import {
SpinnerIcon,
XIcon,
} from '@modrinth/assets'
-import { UserBadge } from '@modrinth/utils'
+import {
+ isModrinthUser as checkIsModrinthUser,
+ isOfficialAccount as checkIsOfficialAccount,
+ UserBadge,
+} from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@@ -716,8 +720,8 @@ const earliestProjectByType = computed(() => {
return earliest
})
-const isModrinthUser = computed(() => user.value?.id === '2REoufqX')
-const isOfficialAccount = computed(() => isModrinthUser.value || user.value?.id === 'GVFjtWTf')
+const isModrinthUser = computed(() => checkIsModrinthUser(user.value?.id))
+const isOfficialAccount = computed(() => checkIsOfficialAccount(user.value?.id))
const isSelf = computed(() => auth.user.value?.id === user.value?.id)
const isAdminViewing = computed(() => auth.user.value?.role === 'admin')
const isStaffViewing = computed(
diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json
index f515082ecb..f413e28eaf 100644
--- a/packages/ui/src/locales/en-US/index.json
+++ b/packages/ui/src/locales/en-US/index.json
@@ -2318,6 +2318,9 @@
"label.password": {
"defaultMessage": "Password"
},
+ "label.permissions": {
+ "defaultMessage": "Permissions"
+ },
"label.plan-custom": {
"defaultMessage": "Custom"
},
diff --git a/packages/ui/src/utils/common-messages.ts b/packages/ui/src/utils/common-messages.ts
index f9d65d0747..26eefd90ad 100644
--- a/packages/ui/src/utils/common-messages.ts
+++ b/packages/ui/src/utils/common-messages.ts
@@ -287,6 +287,10 @@ export const commonMessages = defineMessages({
id: 'label.scopes',
defaultMessage: 'Scopes',
},
+ permissionsLabel: {
+ id: 'label.permissions',
+ defaultMessage: 'Permissions',
+ },
searchLabel: {
id: 'label.search',
defaultMessage: 'Search',
diff --git a/packages/utils/users.ts b/packages/utils/users.ts
index f13f438d81..192955dd9d 100644
--- a/packages/utils/users.ts
+++ b/packages/utils/users.ts
@@ -13,3 +13,17 @@ export const isAdmin = (user) => {
}
export const STAFF_ROLES = ['moderator', 'admin']
+
+export const MODRINTH_USER_ID = '2REoufqX'
+export const AUTOMOD_USER_ID = ''
+export const MODRINTH_ARCHIVES_USER_ID = 'GVFjtWTf'
+
+export const OFFICIAL_ACCOUNT_IDS = [MODRINTH_USER_ID, AUTOMOD_USER_ID, MODRINTH_ARCHIVES_USER_ID]
+
+export const isModrinthUser = (userId) => {
+ return userId === MODRINTH_USER_ID
+}
+
+export const isOfficialAccount = (userId) => {
+ return OFFICIAL_ACCOUNT_IDS.includes(userId)
+}
From 5d34e7902a0171c9f9c4ba994777afe2b18239d5 Mon Sep 17 00:00:00 2001
From: "Calum H."
Date: Tue, 28 Jul 2026 15:09:41 +0100
Subject: [PATCH 002/145] refactor: app settings layout (#6891)
* refactor: app settings layout
* fix: privacy copy got reverted
* fix: i18n
* fix: modal title
* fix: rev comments
---
.../src/components/ui/JavaDetectionModal.vue | 85 ++++-
.../src/components/ui/JavaSelector.vue | 57 ++-
.../components/ui/modal/AppSettingsModal.vue | 115 ++++--
.../ui/settings/AppearanceSettings.vue | 340 ------------------
.../ui/settings/DefaultInstanceSettings.vue | 186 ----------
.../settings/ResourceManagementSettings.vue | 212 -----------
.../{ => account}/PrivacySettings.vue | 51 ++-
.../settings/display/AppearanceSettings.vue | 108 ++++++
.../ui/settings/display/BehaviorSettings.vue | 302 ++++++++++++++++
.../{ => display}/FeatureFlagSettings.vue | 0
.../{ => display}/LanguageSettings.vue | 5 +-
.../instances/DefaultInstanceSettings.vue | 333 +++++++++++++++++
.../settings/{ => instances}/JavaSettings.vue | 12 +-
.../instances/ResourceManagementSettings.vue | 292 +++++++++++++++
apps/app-frontend/src/helpers/settings.ts | 2 +-
.../app-frontend/src/locales/ar-SA/index.json | 3 -
.../app-frontend/src/locales/cs-CZ/index.json | 3 -
.../app-frontend/src/locales/da-DK/index.json | 3 -
.../app-frontend/src/locales/de-CH/index.json | 3 -
.../app-frontend/src/locales/de-DE/index.json | 3 -
.../app-frontend/src/locales/en-US/index.json | 252 +++++++++++--
.../src/locales/es-419/index.json | 3 -
.../app-frontend/src/locales/es-ES/index.json | 3 -
.../app-frontend/src/locales/fi-FI/index.json | 3 -
.../src/locales/fil-PH/index.json | 3 -
.../app-frontend/src/locales/fr-FR/index.json | 3 -
.../app-frontend/src/locales/hu-HU/index.json | 3 -
.../app-frontend/src/locales/id-ID/index.json | 3 -
.../app-frontend/src/locales/it-IT/index.json | 3 -
.../app-frontend/src/locales/ja-JP/index.json | 3 -
.../app-frontend/src/locales/ko-KR/index.json | 3 -
.../app-frontend/src/locales/ms-MY/index.json | 3 -
.../app-frontend/src/locales/nl-NL/index.json | 3 -
.../app-frontend/src/locales/no-NO/index.json | 3 -
.../app-frontend/src/locales/pl-PL/index.json | 3 -
.../app-frontend/src/locales/pt-BR/index.json | 3 -
.../app-frontend/src/locales/pt-PT/index.json | 3 -
.../app-frontend/src/locales/ru-RU/index.json | 3 -
.../app-frontend/src/locales/sr-CS/index.json | 3 -
.../app-frontend/src/locales/sv-SE/index.json | 3 -
.../app-frontend/src/locales/th-TH/index.json | 3 -
.../app-frontend/src/locales/tr-TR/index.json | 3 -
.../app-frontend/src/locales/uk-UA/index.json | 3 -
.../app-frontend/src/locales/vi-VN/index.json | 3 -
.../app-frontend/src/locales/zh-CN/index.json | 3 -
.../app-frontend/src/locales/zh-TW/index.json | 3 -
package.json | 1 +
.../ui/src/components/modal/TabbedModal.vue | 50 ++-
.../src/stories/modal/TabbedModal.stories.ts | 9 +
pnpm-lock.yaml | 104 ++++--
50 files changed, 1645 insertions(+), 961 deletions(-)
delete mode 100644 apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue
delete mode 100644 apps/app-frontend/src/components/ui/settings/DefaultInstanceSettings.vue
delete mode 100644 apps/app-frontend/src/components/ui/settings/ResourceManagementSettings.vue
rename apps/app-frontend/src/components/ui/settings/{ => account}/PrivacySettings.vue (61%)
create mode 100644 apps/app-frontend/src/components/ui/settings/display/AppearanceSettings.vue
create mode 100644 apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue
rename apps/app-frontend/src/components/ui/settings/{ => display}/FeatureFlagSettings.vue (100%)
rename apps/app-frontend/src/components/ui/settings/{ => display}/LanguageSettings.vue (91%)
create mode 100644 apps/app-frontend/src/components/ui/settings/instances/DefaultInstanceSettings.vue
rename apps/app-frontend/src/components/ui/settings/{ => instances}/JavaSettings.vue (73%)
create mode 100644 apps/app-frontend/src/components/ui/settings/instances/ResourceManagementSettings.vue
diff --git a/apps/app-frontend/src/components/ui/JavaDetectionModal.vue b/apps/app-frontend/src/components/ui/JavaDetectionModal.vue
index 3f864c6472..31e481431f 100644
--- a/apps/app-frontend/src/components/ui/JavaDetectionModal.vue
+++ b/apps/app-frontend/src/components/ui/JavaDetectionModal.vue
@@ -1,5 +1,9 @@
-
+
@@ -11,15 +15,23 @@
- Selected
+
+
+ {{ formatMessage(messages.selected) }}
+
- Select
+
+
+ {{ formatMessage(messages.select) }}
+
- No java installations found!
+
+ {{ formatMessage(messages.noInstallationsFound) }}
+
@@ -28,8 +40,8 @@
class="!shadow-none !border-surface-4 !border"
@click="$refs.detectJavaModal.hide()"
>
-
- Cancel
+
+ {{ formatMessage(messages.cancel) }}
@@ -38,23 +50,70 @@
-
+
-
@@ -168,6 +218,7 @@ const messages = defineMessages({
-
+
-
Modrinth App {{ version }}
- macOS
+ {{ formatMessage(messages.appVersion, { version }) }}
+
+
+ {{ formatMessage(messages.macos) }}
{{ osPlatform }}
{{ osVersion }}
diff --git a/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue b/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue
deleted file mode 100644
index 8100571a9a..0000000000
--- a/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue
+++ /dev/null
@@ -1,340 +0,0 @@
-
-
-
- {{ formatMessage(messages.colorThemeTitle) }}
-
- {{ formatMessage(messages.colorThemeDescription) }}
-
-
-
-
-
-
- {{ formatMessage(messages.advancedRenderingTitle) }}
-
-
- {{ formatMessage(messages.advancedRenderingDescription) }}
-
-
-
-
{
- themeStore.advancedRendering = !!e
- settings.advanced_rendering = themeStore.advancedRendering
- }
- "
- />
-
-
-
-
-
- {{ formatMessage(messages.nativeDecorationsTitle) }}
-
-
{{ formatMessage(messages.nativeDecorationsDescription) }}
-
-
-
-
-
-
-
- {{ formatMessage(messages.minimizeLauncherTitle) }}
-
-
{{ formatMessage(messages.minimizeLauncherDescription) }}
-
-
-
-
-
-
-
- {{ formatMessage(messages.showPlayTimeTitle) }}
-
-
{{ formatMessage(messages.showPlayTimeDescription) }}
-
-
{
- const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
- themeStore.featureFlags[showPlayTimeFlag] = newValue
- settings.feature_flags[showPlayTimeFlag] = newValue
- }
- "
- />
-
-
-
-
-
- {{ formatMessage(messages.hideNametagTitle) }}
-
-
{{ formatMessage(messages.hideNametagDescription) }}
-
-
{
- themeStore.hideNametagSkinsPage = !!e
- settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
- }
- "
- />
-
-
-
-
-
- {{ formatMessage(messages.defaultLandingPageTitle) }}
-
-
{{ formatMessage(messages.defaultLandingPageDescription) }}
-
-
-
-
-
-
-
- {{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
-
-
{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}
-
-
{
- const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
- themeStore.featureFlags[worldsInHomeFlag] = newValue
- settings.feature_flags[worldsInHomeFlag] = newValue
- }
- "
- />
-
-
-
-
-
- {{ formatMessage(messages.unknownPackWarningTitle) }}
-
-
{{ formatMessage(messages.unknownPackWarningDescription) }}
-
-
{
- const warnBeforeUnknownPackInstall = !!e
- const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
- themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
- settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
- }
- "
- />
-
-
-
-
-
- {{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
-
-
{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}
-
-
{
- const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
- themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
- settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
- }
- "
- />
-
-
-
-
-
- {{ formatMessage(messages.toggleSidebarTitle) }}
-
-
{{ formatMessage(messages.toggleSidebarDescription) }}
-
-
-
diff --git a/apps/app-frontend/src/components/ui/settings/DefaultInstanceSettings.vue b/apps/app-frontend/src/components/ui/settings/DefaultInstanceSettings.vue
deleted file mode 100644
index 252de30984..0000000000
--- a/apps/app-frontend/src/components/ui/settings/DefaultInstanceSettings.vue
+++ /dev/null
@@ -1,186 +0,0 @@
-
-
-
-
-
-
-
-
Fullscreen
-
- Overwrites the options.txt file to start in full screen when launched.
-
-
-
-
-
-
-
-
-
Width
-
The width of the game window when launched.
-
-
-
-
-
-
-
-
Height
-
The height of the game window when launched.
-
-
-
-
-
-
-
-
-
-
-
Memory allocated
-
-
The memory allocated to each instance when it is ran.
-
-
-
-
Java arguments
-
-
-
-
-
Environmental variables
-
-
-
-
-
-
-
-
-
Pre launch hook
-
-
Ran before the instance is launched.
-
-
-
-
Wrapper hook
-
-
Wrapper command for launching Minecraft.
-
-
-
-
Post exit hook
-
-
Ran after the game closes.
-
-
-
-
diff --git a/apps/app-frontend/src/components/ui/settings/ResourceManagementSettings.vue b/apps/app-frontend/src/components/ui/settings/ResourceManagementSettings.vue
deleted file mode 100644
index 4891ecd2fb..0000000000
--- a/apps/app-frontend/src/components/ui/settings/ResourceManagementSettings.vue
+++ /dev/null
@@ -1,212 +0,0 @@
-
-
-
-
-
-
App directory
-
-
-
-
-
-
-
-
-
-
- The directory where the launcher stores all of its files. Changes will be applied after
- restarting the launcher.
-
-
-
-
-
-
App cache
-
-
- Purge cache
-
-
- The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
- app to reload data. This may slow down the app temporarily.
-
-
-
-
-
Maximum concurrent downloads
-
-
- The maximum amount of files the launcher can download at the same time. Set this to a lower
- value if you have a poor internet connection. (app restart required to take effect)
-
-
-
-
-
Maximum concurrent writes
-
-
- The maximum amount of files the launcher can write to the disk at once. Set this to a lower
- value if you are frequently getting I/O errors. (app restart required to take effect)
-
-
-
-
-
-
- {{ formatMessage(messages.alwaysShowCopyDetailsTitle) }}
-
-
- {{ formatMessage(messages.alwaysShowCopyDetailsDescription) }}
-
-
-
{
- const newValue = !themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)
- themeStore.featureFlags[alwaysShowCopyDetailsFlag] = newValue
- settings.feature_flags[alwaysShowCopyDetailsFlag] = newValue
- }
- "
- />
-
-
-
-
App database backups
-
-
- Open backups folder
-
-
- Backups of important app data are stored here in case you need to recover them later.
-
-
-
-
diff --git a/apps/app-frontend/src/components/ui/settings/PrivacySettings.vue b/apps/app-frontend/src/components/ui/settings/account/PrivacySettings.vue
similarity index 61%
rename from apps/app-frontend/src/components/ui/settings/PrivacySettings.vue
rename to apps/app-frontend/src/components/ui/settings/account/PrivacySettings.vue
index b13cea773d..b945942ae6 100644
--- a/apps/app-frontend/src/components/ui/settings/PrivacySettings.vue
+++ b/apps/app-frontend/src/components/ui/settings/account/PrivacySettings.vue
@@ -33,6 +33,24 @@ const messages = defineMessages({
id: 'app.ads-consent.manage',
defaultMessage: 'Manage preferences',
},
+ telemetryTitle: {
+ id: 'app.settings.privacy.telemetry.title',
+ defaultMessage: 'Telemetry',
+ },
+ telemetryDescription: {
+ id: 'app.settings.privacy.telemetry.description',
+ defaultMessage:
+ 'Modrinth collects anonymized analytics and usage data to improve our user experience and customize your experience. By disabling this option, you opt out and your data will no longer be collected.',
+ },
+ discordRichPresenceTitle: {
+ id: 'app.settings.privacy.discord-rich-presence.title',
+ defaultMessage: 'Discord Rich Presence',
+ },
+ discordRichPresenceDescription: {
+ id: 'app.settings.privacy.discord-rich-presence.description',
+ defaultMessage:
+ 'Show Modrinth App as your current activity on Discord. This does not affect Rich Presence added to instances by mods. Requires an app restart.',
+ },
})
async function manageAdsPreferences() {
@@ -59,28 +77,26 @@ watch(
{{ formatMessage(messages.adsConsentTitle) }}
-
-
-
- {{ formatMessage(messages.adsConsentIntro) }}
-
-
+
{{ formatMessage(messages.adsConsentManage) }}
+
+ {{ formatMessage(messages.adsConsentIntro) }}
+
-
Telemetry
-
- Modrinth collects anonymized analytics and usage data to improve our user experience and
- customize your experience. By disabling this option, you opt out and your data will no
- longer be collected.
+
+ {{ formatMessage(messages.telemetryTitle) }}
+
+
+ {{ formatMessage(messages.telemetryDescription) }}
@@ -88,14 +104,11 @@ watch(
-
Discord RPC
-
- Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
- longer show up as a game or app you are using on your Discord profile.
-
-
- Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
- as those added by mods. (app restart required to take effect)
+
+ {{ formatMessage(messages.discordRichPresenceTitle) }}
+
+
+ {{ formatMessage(messages.discordRichPresenceDescription) }}
diff --git a/apps/app-frontend/src/components/ui/settings/display/AppearanceSettings.vue b/apps/app-frontend/src/components/ui/settings/display/AppearanceSettings.vue
new file mode 100644
index 0000000000..19acfad5ad
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/display/AppearanceSettings.vue
@@ -0,0 +1,108 @@
+
+
+
+ {{ formatMessage(messages.colorThemeTitle) }}
+
+
+ {{ formatMessage(messages.colorThemeDescription) }}
+
+
+
+
+
+
+ {{ formatMessage(messages.advancedRenderingTitle) }}
+
+
+ {{ formatMessage(messages.advancedRenderingDescription) }}
+
+
+
+
{
+ themeStore.advancedRendering = !!e
+ settings.advanced_rendering = themeStore.advancedRendering
+ }
+ "
+ />
+
+
+
+
+
+ {{ formatMessage(messages.nativeDecorationsTitle) }}
+
+
{{ formatMessage(messages.nativeDecorationsDescription) }}
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue b/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue
new file mode 100644
index 0000000000..25a7bc8016
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue
@@ -0,0 +1,302 @@
+
+
+
+
+ {{ formatMessage(messages.startupAndNavigationTitle) }}
+
+
+
+
+ {{ formatMessage(messages.defaultLandingPageTitle) }}
+
+
+
+ {{ formatMessage(messages.defaultLandingPageDescription) }}
+
+
+
+
+
+
+ {{ formatMessage(messages.minimizeLauncherTitle) }}
+
+
+ {{ formatMessage(messages.minimizeLauncherDescription) }}
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.toggleSidebarTitle) }}
+
+
{{ formatMessage(messages.toggleSidebarDescription) }}
+
+
+
+
+
+
+
+ {{ formatMessage(messages.contentTitle) }}
+
+
+
+
+
+ {{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
+
+
+ {{ formatMessage(messages.jumpBackIntoWorldsDescription) }}
+
+
+
{
+ const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
+ themeStore.featureFlags[worldsInHomeFlag] = newValue
+ settings.feature_flags[worldsInHomeFlag] = newValue
+ }
+ "
+ />
+
+
+
+
+
+ {{ formatMessage(messages.showPlayTimeTitle) }}
+
+
{{ formatMessage(messages.showPlayTimeDescription) }}
+
+
{
+ const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
+ themeStore.featureFlags[showPlayTimeFlag] = newValue
+ settings.feature_flags[showPlayTimeFlag] = newValue
+ }
+ "
+ />
+
+
+
+
+
+ {{ formatMessage(messages.hideNametagTitle) }}
+
+
{{ formatMessage(messages.hideNametagDescription) }}
+
+
{
+ themeStore.hideNametagSkinsPage = !!e
+ settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
+ }
+ "
+ />
+
+
+
+
+
+
+ {{ formatMessage(messages.confirmationsTitle) }}
+
+
+
+
+
+ {{ formatMessage(messages.unknownPackWarningTitle) }}
+
+
+ {{ formatMessage(messages.unknownPackWarningDescription) }}
+
+
+
{
+ const warnBeforeUnknownPackInstall = !!e
+ const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
+ themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
+ settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
+ }
+ "
+ />
+
+
+
+
+
+ {{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
+
+
+ {{ formatMessage(messages.skipNonEssentialWarningsDescription) }}
+
+
+
{
+ const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
+ themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
+ settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
+ }
+ "
+ />
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/FeatureFlagSettings.vue b/apps/app-frontend/src/components/ui/settings/display/FeatureFlagSettings.vue
similarity index 100%
rename from apps/app-frontend/src/components/ui/settings/FeatureFlagSettings.vue
rename to apps/app-frontend/src/components/ui/settings/display/FeatureFlagSettings.vue
diff --git a/apps/app-frontend/src/components/ui/settings/LanguageSettings.vue b/apps/app-frontend/src/components/ui/settings/display/LanguageSettings.vue
similarity index 91%
rename from apps/app-frontend/src/components/ui/settings/LanguageSettings.vue
rename to apps/app-frontend/src/components/ui/settings/display/LanguageSettings.vue
index f93bfdc39d..01f4384bcc 100644
--- a/apps/app-frontend/src/components/ui/settings/LanguageSettings.vue
+++ b/apps/app-frontend/src/components/ui/settings/display/LanguageSettings.vue
@@ -2,6 +2,7 @@
import {
Admonition,
AutoLink,
+ commonSettingsMessages,
IntlFormatted,
LanguageSelector,
languageSelectorMessages,
@@ -43,7 +44,9 @@ async function onLocaleChange(newLocale: string) {
- Language
+
+ {{ formatMessage(commonSettingsMessages.language) }}
+
{{ formatMessage(languageSelectorMessages.languageWarning, { platform }) }}
diff --git a/apps/app-frontend/src/components/ui/settings/instances/DefaultInstanceSettings.vue b/apps/app-frontend/src/components/ui/settings/instances/DefaultInstanceSettings.vue
new file mode 100644
index 0000000000..02183800b6
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/DefaultInstanceSettings.vue
@@ -0,0 +1,333 @@
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.fullscreenTitle) }}
+
+
+ {{ formatMessage(messages.fullscreenDescription) }}
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.widthTitle) }}
+
+
+ {{ formatMessage(messages.widthDescription) }}
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.heightTitle) }}
+
+
+ {{ formatMessage(messages.heightDescription) }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.memoryAllocationTitle) }}
+
+
+
+ {{ formatMessage(messages.memoryAllocationDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.javaArgumentsTitle) }}
+
+
+
+ {{ formatMessage(messages.javaArgumentsDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.environmentVariablesTitle) }}
+
+
+
+ {{ formatMessage(messages.environmentVariablesDescription) }}
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.preLaunchHookTitle) }}
+
+
+
+ {{ formatMessage(messages.preLaunchHookDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.wrapperHookTitle) }}
+
+
+
+ {{ formatMessage(messages.wrapperHookDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.postExitHookTitle) }}
+
+
+
+ {{ formatMessage(messages.postExitHookDescription) }}
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/JavaSettings.vue b/apps/app-frontend/src/components/ui/settings/instances/JavaSettings.vue
similarity index 73%
rename from apps/app-frontend/src/components/ui/settings/JavaSettings.vue
rename to apps/app-frontend/src/components/ui/settings/instances/JavaSettings.vue
index 08e8b15257..525197f167 100644
--- a/apps/app-frontend/src/components/ui/settings/JavaSettings.vue
+++ b/apps/app-frontend/src/components/ui/settings/instances/JavaSettings.vue
@@ -1,11 +1,19 @@
+
+
+
+
+
+ {{ formatMessage(messages.appDirectoryTitle) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.appDirectoryDescription) }}
+
+
+
+
+
+
+ {{ formatMessage(messages.alwaysShowCopyDetailsTitle) }}
+
+
+ {{ formatMessage(messages.alwaysShowCopyDetailsDescription) }}
+
+
+
{
+ const newValue = !themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)
+ themeStore.featureFlags[alwaysShowCopyDetailsFlag] = newValue
+ settings.feature_flags[alwaysShowCopyDetailsFlag] = newValue
+ }
+ "
+ />
+
+
+
+
+
+ {{ formatMessage(messages.appCacheTitle) }}
+
+
+
+ {{ formatMessage(messages.purgeCache) }}
+
+
+ {{ formatMessage(messages.appCacheDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.maximumConcurrentDownloadsTitle) }}
+
+
+
+ {{ formatMessage(messages.maximumConcurrentDownloadsDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.maximumConcurrentWritesTitle) }}
+
+
+
+ {{ formatMessage(messages.maximumConcurrentWritesDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.appDatabaseBackupsTitle) }}
+
+
+
+ {{ formatMessage(messages.openBackupsFolder) }}
+
+
+ {{ formatMessage(messages.appDatabaseBackupsDescription) }}
+
+
+
+
diff --git a/apps/app-frontend/src/helpers/settings.ts b/apps/app-frontend/src/helpers/settings.ts
index a63dbe78e3..dd6bdf3ac6 100644
--- a/apps/app-frontend/src/helpers/settings.ts
+++ b/apps/app-frontend/src/helpers/settings.ts
@@ -37,7 +37,7 @@ export type AppSettings = {
theme: ColorTheme
locale: string
- default_page: 'home' | 'library'
+ default_page: 'Home' | 'Library'
collapsed_navigation: boolean
hide_nametag_skins_page: boolean
advanced_rendering: boolean
diff --git a/apps/app-frontend/src/locales/ar-SA/index.json b/apps/app-frontend/src/locales/ar-SA/index.json
index 23bdb64adc..bbfea3026d 100644
--- a/apps/app-frontend/src/locales/ar-SA/index.json
+++ b/apps/app-frontend/src/locales/ar-SA/index.json
@@ -182,9 +182,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "الزخارف التقليدية"
},
- "app.appearance-settings.select-option": {
- "message": "حدد خياراً"
- },
"app.appearance-settings.show-play-time.description": {
"message": "يعرض مقدار الوقت الذي قضيته في لعب إحدى المهام."
},
diff --git a/apps/app-frontend/src/locales/cs-CZ/index.json b/apps/app-frontend/src/locales/cs-CZ/index.json
index bfd1f82488..52a4a49944 100644
--- a/apps/app-frontend/src/locales/cs-CZ/index.json
+++ b/apps/app-frontend/src/locales/cs-CZ/index.json
@@ -113,9 +113,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Systémové dekorace"
},
- "app.appearance-settings.select-option": {
- "message": "Vyberte možnost"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Zobrazuje, kolik času jsi strávil hraním instance."
},
diff --git a/apps/app-frontend/src/locales/da-DK/index.json b/apps/app-frontend/src/locales/da-DK/index.json
index f1f068d1ab..bec940663e 100644
--- a/apps/app-frontend/src/locales/da-DK/index.json
+++ b/apps/app-frontend/src/locales/da-DK/index.json
@@ -173,9 +173,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimer launcher"
},
- "app.appearance-settings.select-option": {
- "message": "Vælg en mulighed"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Vis hvor meget tid du har brugt på at spille en instance."
},
diff --git a/apps/app-frontend/src/locales/de-CH/index.json b/apps/app-frontend/src/locales/de-CH/index.json
index 463d4e8dd7..c38cea0f4a 100644
--- a/apps/app-frontend/src/locales/de-CH/index.json
+++ b/apps/app-frontend/src/locales/de-CH/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Native Dekorationen"
},
- "app.appearance-settings.select-option": {
- "message": "Option wählen"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du mit dem Spielen einer Instanz verbracht hast."
},
diff --git a/apps/app-frontend/src/locales/de-DE/index.json b/apps/app-frontend/src/locales/de-DE/index.json
index 2882f21613..174d8afbe3 100644
--- a/apps/app-frontend/src/locales/de-DE/index.json
+++ b/apps/app-frontend/src/locales/de-DE/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Native Dekorationen"
},
- "app.appearance-settings.select-option": {
- "message": "Wähle eine Option"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du damit verbracht hast, eine Instanz zu spielen."
},
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index cb4965cb85..165832234d 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -144,19 +144,19 @@
"message": "Your privacy and how ads support Modrinth"
},
"app.appearance-settings.advanced-rendering.description": {
- "message": "Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering."
+ "message": "Enable visual effects such as background blur. This may reduce performance without hardware acceleration."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Advanced rendering"
},
"app.appearance-settings.color-theme.description": {
- "message": "Select your preferred color theme for Modrinth App."
+ "message": "Choose the color theme used by Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Color theme"
},
"app.appearance-settings.default-landing-page.description": {
- "message": "Change the page to which the launcher opens on."
+ "message": "Choose the page shown when Modrinth App opens."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Home"
@@ -168,52 +168,49 @@
"message": "Default landing page"
},
"app.appearance-settings.hide-nametag.description": {
- "message": "Disables the nametag above your player on the skins page."
+ "message": "Hide your username above the player preview on the Skin selector page."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Hide nametag"
},
"app.appearance-settings.jump-back-into-worlds.description": {
- "message": "Includes recent worlds in the \"Jump back in\" section on the Home page."
+ "message": "Show recently played worlds in the \"Jump back in\" section on the Home page."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Jump back into worlds"
},
"app.appearance-settings.minimize-launcher.description": {
- "message": "Minimize the launcher when a Minecraft process starts."
+ "message": "Minimize Modrinth App when Minecraft starts."
},
"app.appearance-settings.minimize-launcher.title": {
- "message": "Minimize launcher"
+ "message": "Minimize app"
},
"app.appearance-settings.native-decorations.description": {
- "message": "Use system window frame (app restart required)."
+ "message": "Use your operating system's title bar and window controls. Requires an app restart."
},
"app.appearance-settings.native-decorations.title": {
- "message": "Native decorations"
- },
- "app.appearance-settings.select-option": {
- "message": "Select an option"
+ "message": "System window frame"
},
"app.appearance-settings.show-play-time.description": {
- "message": "Displays how much time you've spent playing an instance."
+ "message": "Show how long you've played each instance."
},
"app.appearance-settings.show-play-time.title": {
"message": "Show play time"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
- "message": "Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown."
+ "message": "Skip confirmations for low-risk actions such as duplicate installs, normal content deletion, bulk updates, unlinking, and repairs. Warnings for dangerous actions are always shown."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Skip non-essential warnings"
},
"app.appearance-settings.toggle-sidebar.description": {
- "message": "Enables the ability to toggle the sidebar."
+ "message": "Hide the right sidebar by default and add a button to show or hide it."
},
"app.appearance-settings.toggle-sidebar.title": {
- "message": "Toggle sidebar"
+ "message": "Hide right sidebar"
},
"app.appearance-settings.unknown-pack-warning.description": {
- "message": "If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it."
+ "message": "Show a safety warning before installing a Modrinth Pack (.mrpack) that isn't hosted on Modrinth."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Warn me before installing unknown modpacks"
@@ -224,6 +221,15 @@
"app.auth-servers.unreachable.header": {
"message": "Cannot reach authentication servers"
},
+ "app.behavior-settings.confirmations.title": {
+ "message": "Confirmations"
+ },
+ "app.behavior-settings.content.title": {
+ "message": "Home and content"
+ },
+ "app.behavior-settings.startup-and-navigation.title": {
+ "message": "Startup and navigation"
+ },
"app.browse.add-servers-to-instance": {
"message": "Adding server to instance"
},
@@ -572,6 +578,51 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Search {count} worlds..."
},
+ "app.java-detection.cancel": {
+ "message": "Cancel"
+ },
+ "app.java-detection.columns.actions": {
+ "message": "Actions"
+ },
+ "app.java-detection.columns.path": {
+ "message": "Path"
+ },
+ "app.java-detection.columns.version": {
+ "message": "Version"
+ },
+ "app.java-detection.no-installations-found": {
+ "message": "No Java installations found."
+ },
+ "app.java-detection.select": {
+ "message": "Select"
+ },
+ "app.java-detection.selected": {
+ "message": "Selected"
+ },
+ "app.java-detection.title": {
+ "message": "Select Java installation"
+ },
+ "app.java-selector.already-installed": {
+ "message": "Already installed"
+ },
+ "app.java-selector.browse": {
+ "message": "Browse"
+ },
+ "app.java-selector.detect": {
+ "message": "Detect"
+ },
+ "app.java-selector.install-recommended": {
+ "message": "Install recommended"
+ },
+ "app.java-selector.installing": {
+ "message": "Installing..."
+ },
+ "app.java-selector.path.placeholder": {
+ "message": "/path/to/java"
+ },
+ "app.java-selector.test-installation": {
+ "message": "Test Java installation"
+ },
"app.modal.install-to-play.additional-context": {
"message": "Additional context"
},
@@ -704,11 +755,86 @@
"app.project.versions.already-installed": {
"message": "Already installed"
},
- "app.resource-management-settings.always-show-copy-details.description": {
- "message": "Show the Copy details action while an install is queued or running. It is always available for failed or interrupted installs."
+ "app.settings.app-version": {
+ "message": "Modrinth App {version}"
},
- "app.resource-management-settings.always-show-copy-details.title": {
- "message": "Always show copy details"
+ "app.settings.default-instance-options.environment-variables.description": {
+ "message": "Environment variables set when launching an instance."
+ },
+ "app.settings.default-instance-options.environment-variables.placeholder": {
+ "message": "Enter environment variables..."
+ },
+ "app.settings.default-instance-options.environment-variables.title": {
+ "message": "Environment variables"
+ },
+ "app.settings.default-instance-options.fullscreen.description": {
+ "message": "Start instances in fullscreen by updating their options.txt file."
+ },
+ "app.settings.default-instance-options.fullscreen.title": {
+ "message": "Fullscreen"
+ },
+ "app.settings.default-instance-options.height.description": {
+ "message": "The height of the game window when launched."
+ },
+ "app.settings.default-instance-options.height.placeholder": {
+ "message": "Enter height..."
+ },
+ "app.settings.default-instance-options.height.title": {
+ "message": "Height"
+ },
+ "app.settings.default-instance-options.java-arguments.description": {
+ "message": "Arguments passed to Java when launching an instance."
+ },
+ "app.settings.default-instance-options.java-arguments.placeholder": {
+ "message": "Enter Java arguments..."
+ },
+ "app.settings.default-instance-options.java-arguments.title": {
+ "message": "Java arguments"
+ },
+ "app.settings.default-instance-options.memory-allocation.description": {
+ "message": "Maximum memory available to each instance."
+ },
+ "app.settings.default-instance-options.memory-allocation.title": {
+ "message": "Memory allocation"
+ },
+ "app.settings.default-instance-options.post-exit-hook.description": {
+ "message": "Runs after the game closes."
+ },
+ "app.settings.default-instance-options.post-exit-hook.placeholder": {
+ "message": "Enter post-exit command..."
+ },
+ "app.settings.default-instance-options.post-exit-hook.title": {
+ "message": "Post-exit hook"
+ },
+ "app.settings.default-instance-options.pre-launch-hook.description": {
+ "message": "Runs before the instance starts."
+ },
+ "app.settings.default-instance-options.pre-launch-hook.placeholder": {
+ "message": "Enter pre-launch command..."
+ },
+ "app.settings.default-instance-options.pre-launch-hook.title": {
+ "message": "Pre-launch hook"
+ },
+ "app.settings.default-instance-options.width.description": {
+ "message": "The width of the game window when launched."
+ },
+ "app.settings.default-instance-options.width.placeholder": {
+ "message": "Enter width..."
+ },
+ "app.settings.default-instance-options.width.title": {
+ "message": "Width"
+ },
+ "app.settings.default-instance-options.wrapper-hook.description": {
+ "message": "Command used to wrap the Minecraft launch process."
+ },
+ "app.settings.default-instance-options.wrapper-hook.placeholder": {
+ "message": "Enter wrapper command..."
+ },
+ "app.settings.default-instance-options.wrapper-hook.title": {
+ "message": "Wrapper hook"
+ },
+ "app.settings.developer-mode-button.label": {
+ "message": "Toggle developer mode"
},
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
@@ -716,12 +842,90 @@
"app.settings.downloading": {
"message": "Downloading v{version}"
},
+ "app.settings.java-installations.location.title": {
+ "message": "Java {version, number} location"
+ },
+ "app.settings.operating-system.macos": {
+ "message": "macOS"
+ },
"app.settings.privacy.ads-consent.intro": {
"message": "Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance. You can opt out or manage your preferences below."
},
+ "app.settings.privacy.discord-rich-presence.description": {
+ "message": "Show Modrinth App as your current activity on Discord. This does not affect Rich Presence added to instances by mods. Requires an app restart."
+ },
+ "app.settings.privacy.discord-rich-presence.title": {
+ "message": "Discord Rich Presence"
+ },
+ "app.settings.privacy.telemetry.description": {
+ "message": "Modrinth collects anonymized analytics and usage data to improve our user experience and customize your experience. By disabling this option, you opt out and your data will no longer be collected."
+ },
+ "app.settings.privacy.telemetry.title": {
+ "message": "Telemetry"
+ },
+ "app.settings.resource-management.always-show-copy-details.description": {
+ "message": "Show the Copy details action while an install is queued or running. It is always available for failed or interrupted installs."
+ },
+ "app.settings.resource-management.always-show-copy-details.title": {
+ "message": "Always show copy details"
+ },
+ "app.settings.resource-management.app-cache.confirm.description": {
+ "message": "The app may load more slowly until the cache is rebuilt."
+ },
+ "app.settings.resource-management.app-cache.confirm.title": {
+ "message": "Purge the app cache?"
+ },
+ "app.settings.resource-management.app-cache.description": {
+ "message": "Clear cached data and download it again from Modrinth. The app may load more slowly until the cache is rebuilt."
+ },
+ "app.settings.resource-management.app-cache.purge": {
+ "message": "Purge cache"
+ },
+ "app.settings.resource-management.app-cache.title": {
+ "message": "App cache"
+ },
+ "app.settings.resource-management.app-database-backups.description": {
+ "message": "Backups of important app data are stored here in case you need to recover them later."
+ },
+ "app.settings.resource-management.app-database-backups.open-folder": {
+ "message": "Open backups folder"
+ },
+ "app.settings.resource-management.app-database-backups.title": {
+ "message": "App database backups"
+ },
+ "app.settings.resource-management.app-directory.browse": {
+ "message": "Browse for an app directory"
+ },
+ "app.settings.resource-management.app-directory.description": {
+ "message": "Where Modrinth App stores instances and other files. Changes take effect after restarting the app."
+ },
+ "app.settings.resource-management.app-directory.select": {
+ "message": "Select a new app directory"
+ },
+ "app.settings.resource-management.app-directory.title": {
+ "message": "App directory"
+ },
+ "app.settings.resource-management.maximum-concurrent-downloads.description": {
+ "message": "Number of files the app can download at once. Lower this if downloads are unreliable on your connection. Requires an app restart."
+ },
+ "app.settings.resource-management.maximum-concurrent-downloads.title": {
+ "message": "Maximum concurrent downloads"
+ },
+ "app.settings.resource-management.maximum-concurrent-writes.description": {
+ "message": "Number of files the app can write to disk at once. Lower this if you frequently encounter I/O errors. Requires an app restart."
+ },
+ "app.settings.resource-management.maximum-concurrent-writes.title": {
+ "message": "Maximum concurrent writes"
+ },
+ "app.settings.sidebar.label.instances": {
+ "message": "Instances"
+ },
"app.settings.tabs.appearance": {
"message": "Appearance"
},
+ "app.settings.tabs.behavior": {
+ "message": "Behavior"
+ },
"app.settings.tabs.default-instance-options": {
"message": "Default instance options"
},
@@ -1564,5 +1768,11 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader is provided by the server"
+ },
+ "settings.sidebar.label.account": {
+ "message": "Account"
+ },
+ "settings.sidebar.label.display": {
+ "message": "Display"
}
}
diff --git a/apps/app-frontend/src/locales/es-419/index.json b/apps/app-frontend/src/locales/es-419/index.json
index d47072d96c..90dd37f992 100644
--- a/apps/app-frontend/src/locales/es-419/index.json
+++ b/apps/app-frontend/src/locales/es-419/index.json
@@ -173,9 +173,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
},
- "app.appearance-settings.select-option": {
- "message": "Selecciona una opción"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
diff --git a/apps/app-frontend/src/locales/es-ES/index.json b/apps/app-frontend/src/locales/es-ES/index.json
index 3a37463c95..2cc03c5e9c 100644
--- a/apps/app-frontend/src/locales/es-ES/index.json
+++ b/apps/app-frontend/src/locales/es-ES/index.json
@@ -107,9 +107,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
},
- "app.appearance-settings.select-option": {
- "message": "Selecciona una opción"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
diff --git a/apps/app-frontend/src/locales/fi-FI/index.json b/apps/app-frontend/src/locales/fi-FI/index.json
index d518abe29a..d9bf90d407 100644
--- a/apps/app-frontend/src/locales/fi-FI/index.json
+++ b/apps/app-frontend/src/locales/fi-FI/index.json
@@ -119,9 +119,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Natiivit koristukset"
},
- "app.appearance-settings.select-option": {
- "message": "Valitse vaihtoehto"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Näyttää, kuinka paljon aikaa olet käyttänyt instanssin pelaamiseen."
},
diff --git a/apps/app-frontend/src/locales/fil-PH/index.json b/apps/app-frontend/src/locales/fil-PH/index.json
index 19dc29d620..da6c9f032b 100644
--- a/apps/app-frontend/src/locales/fil-PH/index.json
+++ b/apps/app-frontend/src/locales/fil-PH/index.json
@@ -95,9 +95,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Native decorations"
},
- "app.appearance-settings.select-option": {
- "message": "Pumili ng opsiyon"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Magpapakita ang katagal ng iyong paglalaro sa isang instansiya."
},
diff --git a/apps/app-frontend/src/locales/fr-FR/index.json b/apps/app-frontend/src/locales/fr-FR/index.json
index ebd4b6cafe..8a863615be 100644
--- a/apps/app-frontend/src/locales/fr-FR/index.json
+++ b/apps/app-frontend/src/locales/fr-FR/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Décorations natives"
},
- "app.appearance-settings.select-option": {
- "message": "Sélectionnez une option"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Affiche combien de temps vous avez passé sur une instance."
},
diff --git a/apps/app-frontend/src/locales/hu-HU/index.json b/apps/app-frontend/src/locales/hu-HU/index.json
index 7501504381..ff55d16202 100644
--- a/apps/app-frontend/src/locales/hu-HU/index.json
+++ b/apps/app-frontend/src/locales/hu-HU/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Rendszer ablakkeret használata"
},
- "app.appearance-settings.select-option": {
- "message": "Válassz egy lehetőséget"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Megmutatja, mennyi időt töltöttél egy játékpéldányban."
},
diff --git a/apps/app-frontend/src/locales/id-ID/index.json b/apps/app-frontend/src/locales/id-ID/index.json
index dc8a98048f..968e0d9573 100644
--- a/apps/app-frontend/src/locales/id-ID/index.json
+++ b/apps/app-frontend/src/locales/id-ID/index.json
@@ -92,9 +92,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
- "app.appearance-settings.select-option": {
- "message": "Pilih opsi"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Menampilkan seberapa lama Anda memainkan sebuah instans."
},
diff --git a/apps/app-frontend/src/locales/it-IT/index.json b/apps/app-frontend/src/locales/it-IT/index.json
index 970ab532ea..8908398c27 100644
--- a/apps/app-frontend/src/locales/it-IT/index.json
+++ b/apps/app-frontend/src/locales/it-IT/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decorazioni native"
},
- "app.appearance-settings.select-option": {
- "message": "Seleziona"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Mostra quanto tempo hai speso giocando a un'istanza."
},
diff --git a/apps/app-frontend/src/locales/ja-JP/index.json b/apps/app-frontend/src/locales/ja-JP/index.json
index 3e5049b361..81e45f7f02 100644
--- a/apps/app-frontend/src/locales/ja-JP/index.json
+++ b/apps/app-frontend/src/locales/ja-JP/index.json
@@ -188,9 +188,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "OSのウィンドウUIを使用"
},
- "app.appearance-settings.select-option": {
- "message": "オプションを選択してください"
- },
"app.appearance-settings.show-play-time.description": {
"message": "インスタンスのプレイ時間を表示します。"
},
diff --git a/apps/app-frontend/src/locales/ko-KR/index.json b/apps/app-frontend/src/locales/ko-KR/index.json
index 9ab40ec035..07a99652ba 100644
--- a/apps/app-frontend/src/locales/ko-KR/index.json
+++ b/apps/app-frontend/src/locales/ko-KR/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "네이티브 제목 표시줄"
},
- "app.appearance-settings.select-option": {
- "message": "옵션 선택"
- },
"app.appearance-settings.show-play-time.description": {
"message": "인스턴스의 플레이 시간을 표시합니다."
},
diff --git a/apps/app-frontend/src/locales/ms-MY/index.json b/apps/app-frontend/src/locales/ms-MY/index.json
index 9d63ae66e3..541d561206 100644
--- a/apps/app-frontend/src/locales/ms-MY/index.json
+++ b/apps/app-frontend/src/locales/ms-MY/index.json
@@ -92,9 +92,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
- "app.appearance-settings.select-option": {
- "message": "Pilih satu pilihan"
- },
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Melangkau pengesahan berisiko rendah secara automatik seperti pemasangan pek mod pendua, pemadaman kandungan biasa, kemas kini pukal, menyahpautkan pek mod dan gesaan pembaikan. Amaran berbahaya masih akan ditunjukkan."
},
diff --git a/apps/app-frontend/src/locales/nl-NL/index.json b/apps/app-frontend/src/locales/nl-NL/index.json
index 1580290671..f7f3dad5ef 100644
--- a/apps/app-frontend/src/locales/nl-NL/index.json
+++ b/apps/app-frontend/src/locales/nl-NL/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Oorspronkelijke versieringen"
},
- "app.appearance-settings.select-option": {
- "message": "Selecteer een optie"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Toont hoeveel tijd je aan een instantie hebt besteed."
},
diff --git a/apps/app-frontend/src/locales/no-NO/index.json b/apps/app-frontend/src/locales/no-NO/index.json
index ca5141df17..3733ad6195 100644
--- a/apps/app-frontend/src/locales/no-NO/index.json
+++ b/apps/app-frontend/src/locales/no-NO/index.json
@@ -89,9 +89,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimer launcheren"
},
- "app.appearance-settings.select-option": {
- "message": "Velg et alternativ"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Viser hvor lenge du har spilt et tilfelle."
},
diff --git a/apps/app-frontend/src/locales/pl-PL/index.json b/apps/app-frontend/src/locales/pl-PL/index.json
index 3a3d29cf75..8e415dc95e 100644
--- a/apps/app-frontend/src/locales/pl-PL/index.json
+++ b/apps/app-frontend/src/locales/pl-PL/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Systemowe dekoracje"
},
- "app.appearance-settings.select-option": {
- "message": "Wybierz opcję"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Pokazuje czas gry spędzony w danej instancji."
},
diff --git a/apps/app-frontend/src/locales/pt-BR/index.json b/apps/app-frontend/src/locales/pt-BR/index.json
index 0215e00b66..c5a9ae394d 100644
--- a/apps/app-frontend/src/locales/pt-BR/index.json
+++ b/apps/app-frontend/src/locales/pt-BR/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decorações nativas"
},
- "app.appearance-settings.select-option": {
- "message": "Selecione uma opção"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Exibe a quantidade de tempo jogado de uma instância."
},
diff --git a/apps/app-frontend/src/locales/pt-PT/index.json b/apps/app-frontend/src/locales/pt-PT/index.json
index d8d2197d6f..247cc2f66e 100644
--- a/apps/app-frontend/src/locales/pt-PT/index.json
+++ b/apps/app-frontend/src/locales/pt-PT/index.json
@@ -188,9 +188,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decorações nativas"
},
- "app.appearance-settings.select-option": {
- "message": "Seleciona uma opção"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Apresenta quanto tempo passou a jogar uma instância."
},
diff --git a/apps/app-frontend/src/locales/ru-RU/index.json b/apps/app-frontend/src/locales/ru-RU/index.json
index 14cf24865f..acccf8358e 100644
--- a/apps/app-frontend/src/locales/ru-RU/index.json
+++ b/apps/app-frontend/src/locales/ru-RU/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Системное оформление"
},
- "app.appearance-settings.select-option": {
- "message": "Выберите вариант"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Показывать время, проведённое в сборке."
},
diff --git a/apps/app-frontend/src/locales/sr-CS/index.json b/apps/app-frontend/src/locales/sr-CS/index.json
index ab4466ae3a..8f6db8dd2f 100644
--- a/apps/app-frontend/src/locales/sr-CS/index.json
+++ b/apps/app-frontend/src/locales/sr-CS/index.json
@@ -173,9 +173,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Zavičajni ukrasi"
},
- "app.appearance-settings.select-option": {
- "message": "Izaberi opciju"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Pokazuje koliko vremena si proveo igrajući instancu."
},
diff --git a/apps/app-frontend/src/locales/sv-SE/index.json b/apps/app-frontend/src/locales/sv-SE/index.json
index 250560722c..ddc5ebb6ca 100644
--- a/apps/app-frontend/src/locales/sv-SE/index.json
+++ b/apps/app-frontend/src/locales/sv-SE/index.json
@@ -173,9 +173,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Inbyggda fönsterdekorationer"
},
- "app.appearance-settings.select-option": {
- "message": "Välj ett alternativ"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Visar hur mycket tid du spelat på en instans."
},
diff --git a/apps/app-frontend/src/locales/th-TH/index.json b/apps/app-frontend/src/locales/th-TH/index.json
index 85eaa85288..44ef707409 100644
--- a/apps/app-frontend/src/locales/th-TH/index.json
+++ b/apps/app-frontend/src/locales/th-TH/index.json
@@ -107,9 +107,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "ของตกแต่งดั้งเดิม"
},
- "app.appearance-settings.select-option": {
- "message": "เลือกตัวเลือก"
- },
"app.appearance-settings.show-play-time.description": {
"message": "แสดงเวลาที่คุณใช้เล่นในแต่ละอินสแตนซ์"
},
diff --git a/apps/app-frontend/src/locales/tr-TR/index.json b/apps/app-frontend/src/locales/tr-TR/index.json
index dcf9ab21bd..ee6118c33f 100644
--- a/apps/app-frontend/src/locales/tr-TR/index.json
+++ b/apps/app-frontend/src/locales/tr-TR/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Yerel pencere öğeleri"
},
- "app.appearance-settings.select-option": {
- "message": "Bir seçenek seçin"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Bir oyun oturumunda ne kadar zaman geçirdiğinizi gösterir."
},
diff --git a/apps/app-frontend/src/locales/uk-UA/index.json b/apps/app-frontend/src/locales/uk-UA/index.json
index aaa3544f91..02699395a4 100644
--- a/apps/app-frontend/src/locales/uk-UA/index.json
+++ b/apps/app-frontend/src/locales/uk-UA/index.json
@@ -188,9 +188,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Системне оформлення"
},
- "app.appearance-settings.select-option": {
- "message": "Оберіть опцію"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Показує, скільки часу ви провели в цьому профілі."
},
diff --git a/apps/app-frontend/src/locales/vi-VN/index.json b/apps/app-frontend/src/locales/vi-VN/index.json
index b19d06752f..a7bc63a113 100644
--- a/apps/app-frontend/src/locales/vi-VN/index.json
+++ b/apps/app-frontend/src/locales/vi-VN/index.json
@@ -182,9 +182,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Giao diện cửa sổ hệ thống"
},
- "app.appearance-settings.select-option": {
- "message": "Chọn một tuỳ chọn"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Hiển thị thời gian bạn đã dành để chơi bằng một cấu hình."
},
diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json
index 76f805ce11..964249316b 100644
--- a/apps/app-frontend/src/locales/zh-CN/index.json
+++ b/apps/app-frontend/src/locales/zh-CN/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "原生窗口"
},
- "app.appearance-settings.select-option": {
- "message": "选择一个选项"
- },
"app.appearance-settings.show-play-time.description": {
"message": "显示你在每个实例中游玩的时间。"
},
diff --git a/apps/app-frontend/src/locales/zh-TW/index.json b/apps/app-frontend/src/locales/zh-TW/index.json
index 81c72dbdaa..87570b3e0f 100644
--- a/apps/app-frontend/src/locales/zh-TW/index.json
+++ b/apps/app-frontend/src/locales/zh-TW/index.json
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "原生視窗"
},
- "app.appearance-settings.select-option": {
- "message": "選擇選項"
- },
"app.appearance-settings.show-play-time.description": {
"message": "顯示你在該實例的遊玩時數。"
},
diff --git a/package.json b/package.json
index 05de315e58..edebadb426 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"@modrinth/tooling-config": "workspace:*",
"@tailwindcss/container-queries": "^0.1.1",
"@types/node": "^24",
+ "@typescript-eslint/typescript-estree": "^8.64.0",
"@vue/compiler-dom": "^3.5.26",
"@vue/compiler-sfc": "^3.5.26",
"chalk": "^5.6.2",
diff --git a/packages/ui/src/components/modal/TabbedModal.vue b/packages/ui/src/components/modal/TabbedModal.vue
index 538676d640..465ffb1226 100644
--- a/packages/ui/src/components/modal/TabbedModal.vue
+++ b/packages/ui/src/components/modal/TabbedModal.vue
@@ -9,6 +9,7 @@ import { useScrollIndicator } from '../../composables/scroll-indicator'
import NewModal from './NewModal.vue'
export interface Tab {
name: MessageDescriptor
+ category?: MessageDescriptor
icon: Component
content?: Component
href?: string
@@ -61,6 +62,11 @@ function hide() {
modal.value?.hide()
}
+function startsCategory(index: number) {
+ const category = visibleTabs.value[index]?.category
+ return !!category && category.id !== visibleTabs.value[index - 1]?.category?.id
+}
+
defineExpose({ show, hide, selectedTab, setTab })
@@ -81,26 +87,32 @@ defineExpose({ show, hide, selectedTab, setTab })
-
-
- {{ formatMessage(tab.name) }}
-
+
- {{ formatMessage(tab.badge) }}
-
-
-
+ {{ formatMessage(tab.category) }}
+
+
+
+ {{ formatMessage(tab.name) }}
+
+ {{ formatMessage(tab.badge) }}
+
+
+
+
diff --git a/packages/ui/src/stories/modal/TabbedModal.stories.ts b/packages/ui/src/stories/modal/TabbedModal.stories.ts
index 6503064cf1..7ed018ee8c 100644
--- a/packages/ui/src/stories/modal/TabbedModal.stories.ts
+++ b/packages/ui/src/stories/modal/TabbedModal.stories.ts
@@ -232,46 +232,55 @@ export const ManyTabs: StoryObj = {
const tabs = [
{
name: { id: 'general', defaultMessage: 'General' },
+ category: { id: 'display-category', defaultMessage: 'Display' },
icon: InfoIcon,
content: makeTabContent('General'),
},
{
name: { id: 'appearance', defaultMessage: 'Appearance' },
+ category: { id: 'display-category', defaultMessage: 'Display' },
icon: PaintbrushIcon,
content: makeTabContent('Appearance'),
},
{
name: { id: 'language', defaultMessage: 'Language' },
+ category: { id: 'display-category', defaultMessage: 'Display' },
icon: LanguagesIcon,
content: makeTabContent('Language'),
},
{
name: { id: 'privacy', defaultMessage: 'Privacy' },
+ category: { id: 'account-category', defaultMessage: 'Account' },
icon: ShieldIcon,
content: makeTabContent('Privacy'),
},
{
name: { id: 'java', defaultMessage: 'Java and memory' },
+ category: { id: 'instances-category', defaultMessage: 'Instances' },
icon: CoffeeIcon,
content: makeTabContent('Java and memory'),
},
{
name: { id: 'instances', defaultMessage: 'Default instance options' },
+ category: { id: 'instances-category', defaultMessage: 'Instances' },
icon: GameIcon,
content: makeTabContent('Default instance options'),
},
{
name: { id: 'resources', defaultMessage: 'Resource management' },
+ category: { id: 'instances-category', defaultMessage: 'Instances' },
icon: GaugeIcon,
content: makeTabContent('Resource management'),
},
{
name: { id: 'window', defaultMessage: 'Window' },
+ category: { id: 'advanced-category', defaultMessage: 'Advanced' },
icon: MonitorIcon,
content: makeTabContent('Window'),
},
{
name: { id: 'hooks', defaultMessage: 'Launch hooks' },
+ category: { id: 'advanced-category', defaultMessage: 'Advanced' },
icon: WrenchIcon,
content: makeTabContent('Launch hooks'),
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3d8dbc26de..72a32c7ed6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -31,6 +31,9 @@ importers:
'@types/node':
specifier: ^24
version: 24.12.2
+ '@typescript-eslint/typescript-estree':
+ specifier: ^8.64.0
+ version: 8.64.0(typescript@5.9.3)
'@vue/compiler-dom':
specifier: ^3.5.26
version: 3.5.27
@@ -4536,6 +4539,12 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
+ '@typescript-eslint/project-service@8.64.0':
+ resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
'@typescript-eslint/scope-manager@8.54.0':
resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -4546,6 +4555,12 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
+ '@typescript-eslint/tsconfig-utils@8.64.0':
+ resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
'@typescript-eslint/type-utils@8.54.0':
resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -4557,12 +4572,22 @@ packages:
resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@typescript-eslint/types@8.64.0':
+ resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
'@typescript-eslint/typescript-estree@8.54.0':
resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
+ '@typescript-eslint/typescript-estree@8.64.0':
+ resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
'@typescript-eslint/utils@8.54.0':
resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -4574,6 +4599,10 @@ packages:
resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@typescript-eslint/visitor-keys@8.64.0':
+ resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -6144,6 +6173,10 @@ packages:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ eslint-visitor-keys@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
eslint@9.39.2:
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -9249,6 +9282,12 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
+ ts-api-utils@2.5.0:
+ resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
ts-dedent@2.2.0:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
engines: {node: '>=6.10'}
@@ -9953,8 +9992,8 @@ packages:
vue-component-type-helpers@3.2.4:
resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
- vue-component-type-helpers@3.3.5:
- resolution: {integrity: sha512-Fe1jyPJoUGpJOYKOri44jduR7My4yYINOMJISuMAbmrs+L5LbIDUc8NTWZYY3EJLK0yPLuCmcd5zoCsE4k2/KA==}
+ vue-component-type-helpers@3.3.7:
+ resolution: {integrity: sha512-Skkhw9agYSgsWqv7bxSOGJZa9SaiJbZVGdXuFWnrzKaQYHnw9qbjD630rw6RyMqDbp54nfLCLw5SZA55if7JLg==}
vue-confetti-explosion@1.0.2:
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
@@ -13436,7 +13475,7 @@ snapshots:
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
type-fest: 2.19.0
vue: 3.5.27(typescript@5.9.3)
- vue-component-type-helpers: 3.3.5
+ vue-component-type-helpers: 3.3.7
'@stripe/stripe-js@7.9.0': {}
@@ -13891,6 +13930,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@typescript-eslint/project-service@8.64.0(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3)
+ '@typescript-eslint/types': 8.64.0
+ debug: 4.4.3
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
'@typescript-eslint/scope-manager@8.54.0':
dependencies:
'@typescript-eslint/types': 8.54.0
@@ -13900,6 +13948,10 @@ snapshots:
dependencies:
typescript: 5.9.3
+ '@typescript-eslint/tsconfig-utils@8.64.0(typescript@5.9.3)':
+ dependencies:
+ typescript: 5.9.3
+
'@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.54.0
@@ -13914,6 +13966,8 @@ snapshots:
'@typescript-eslint/types@8.54.0': {}
+ '@typescript-eslint/types@8.64.0': {}
+
'@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.54.0(typescript@5.9.3)
@@ -13929,6 +13983,21 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@typescript-eslint/typescript-estree@8.64.0(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.64.0(typescript@5.9.3)
+ '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3)
+ '@typescript-eslint/types': 8.64.0
+ '@typescript-eslint/visitor-keys': 8.64.0
+ debug: 4.4.3
+ minimatch: 10.2.3
+ semver: 7.7.4
+ tinyglobby: 0.2.15
+ ts-api-utils: 2.5.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
'@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
@@ -13956,6 +14025,11 @@ snapshots:
'@typescript-eslint/types': 8.54.0
eslint-visitor-keys: 4.2.1
+ '@typescript-eslint/visitor-keys@8.64.0':
+ dependencies:
+ '@typescript-eslint/types': 8.64.0
+ eslint-visitor-keys: 5.0.1
+
'@ungap/structured-clone@1.3.0': {}
'@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3))':
@@ -15815,6 +15889,8 @@ snapshots:
eslint-visitor-keys@4.2.1: {}
+ eslint-visitor-keys@5.0.1: {}
+
eslint@9.39.2(jiti@1.21.7):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
@@ -19773,6 +19849,10 @@ snapshots:
dependencies:
typescript: 5.9.3
+ ts-api-utils@2.5.0(typescript@5.9.3):
+ dependencies:
+ typescript: 5.9.3
+
ts-dedent@2.2.0: {}
ts-interface-checker@0.1.13: {}
@@ -20277,22 +20357,6 @@ snapshots:
yaml: 2.8.2
optional: true
- vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2):
- dependencies:
- lightningcss: 1.32.0
- picomatch: 4.0.4
- postcss: 8.5.8
- rolldown: 1.0.0-rc.12
- tinyglobby: 0.2.15
- optionalDependencies:
- '@types/node': 24.12.2
- esbuild: 0.27.3
- fsevents: 2.3.3
- jiti: 1.21.7
- sass: 1.97.3
- terser: 5.46.0
- yaml: 2.8.2
-
vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2):
dependencies:
lightningcss: 1.32.0
@@ -20429,7 +20493,7 @@ snapshots:
vue-component-type-helpers@3.2.4: {}
- vue-component-type-helpers@3.3.5: {}
+ vue-component-type-helpers@3.3.7: {}
vue-confetti-explosion@1.0.2(vue@3.5.27(typescript@5.9.3)):
dependencies:
From 82348fe40a9a88c75b722427527d5aca006aa381 Mon Sep 17 00:00:00 2001
From: "Calum H."
Date: Tue, 28 Jul 2026 15:10:12 +0100
Subject: [PATCH 003/145] qa: shared instances post release (#6893)
* fix: 20 user cap
* qa: invite modal dropdown + other + clamp limits
* fix: hide filters if just one type
* feat: update req banner
* feat: invite management frontend
* fix: moderation issues with shared instances
* feat: instance icon improvements
* fix: better offline handling
* fix: lint
* feat: show avatar in install to play modal
* feat: smaller qa points
* fix: fmt
* fix: fmt
* fix: yeet svg
* fix: 50 user limit
---
Cargo.lock | 7 +-
.../ui/instance-page-header/index.vue | 2 +-
.../instance/instance-admonitions/index.vue | 25 ++
.../instance-admonitions-messages.ts | 13 +
...tions-shared-instance-update-available.vue | 34 ++
.../ui/instance/instance-admonitions/types.ts | 1 +
.../ui/instance_settings/GeneralSettings.vue | 21 +-
.../ui/instance_settings/SharingSettings.vue | 174 +++++++++-
...ConfirmRevokeSharedInstanceInviteModal.vue | 82 +++++
.../SharedInstanceUpdateModal.vue | 8 +-
.../shared-instance-install-modal/index.vue | 47 ++-
.../use-shared-instance-preview-content.ts | 15 +-
.../use-shared-instance-invite-handler.ts | 71 +++-
apps/app-frontend/src/helpers/install.ts | 5 +
apps/app-frontend/src/helpers/instance.ts | 20 ++
.../src/helpers/shared-instance-errors.ts | 28 +-
.../app-frontend/src/locales/en-US/index.json | 72 ++++
.../app-frontend/src/pages/instance/Index.vue | 56 ++-
.../src/pages/instance/share/index.vue | 103 +++++-
.../share/shared-instance-members-table.vue | 24 +-
.../share/shared-instance-share-types.ts | 2 +
.../share/use-shared-instance-invite-link.ts | 11 +-
.../share/use-shared-instance-members.ts | 8 +-
apps/app/build.rs | 2 +
apps/app/src/api/instance.rs | 23 ++
apps/app/src/api/mod.rs | 15 +-
.../ui/moderation/ModerationReportCard.vue | 15 +-
.../src/components/ui/report/ReportInfo.vue | 14 +-
.../src/components/ui/report/ReportView.vue | 13 +-
.../src/components/ui/report/ReportsList.vue | 33 +-
apps/frontend/src/pages/dashboard/reports.vue | 10 +-
.../src/pages/moderation/reports/index.vue | 9 +-
apps/labrinth/src/queue/email/templates.rs | 87 ++++-
apps/labrinth/src/routes/v2/reports.rs | 5 +-
apps/labrinth/src/routes/v3/reports.rs | 5 +-
packages/app-lib/Cargo.toml | 1 +
packages/app-lib/src/api/instance.rs | 25 +-
packages/app-lib/src/api/instance/icon.rs | 318 ++++++++++++++++++
.../app-lib/src/api/instance/lifecycle.rs | 56 ---
.../app-lib/src/api/instance/shared/client.rs | 65 +++-
.../src/api/instance/shared/install.rs | 9 +-
.../src/api/instance/shared/invites.rs | 38 +++
.../app-lib/src/api/instance/shared/mod.rs | 14 +-
.../app-lib/src/api/instance/shared/types.rs | 9 +
.../app-lib/src/api/pack/import/curseforge.rs | 19 +-
packages/app-lib/src/api/pack/import/mod.rs | 15 +-
packages/app-lib/src/api/pack/install_from.rs | 18 +-
packages/app-lib/src/error.rs | 3 +
packages/app-lib/src/install/runner.rs | 1 +
.../instances/commands/create_instance.rs | 26 +-
packages/app-lib/src/state/mod.rs | 4 +
packages/app-lib/src/util/fetch.rs | 30 +-
packages/ui/src/components/base/CopyCode.vue | 16 +-
.../ui/src/components/base/StyledInput.vue | 24 +-
.../sharing/invite-players-modal/index.vue | 3 +
...nvite-players-modal-invite-link-editor.vue | 280 +++++++++++++--
.../components/ContentCardItem.vue | 29 +-
.../components/ContentCardTable.vue | 2 +
.../components/modals/ModpackContentModal.vue | 18 +-
.../src/layouts/shared/content-tab/types.ts | 6 +
packages/ui/src/locales/en-US/index.json | 27 ++
61 files changed, 1809 insertions(+), 307 deletions(-)
create mode 100644 apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-update-available.vue
create mode 100644 apps/app-frontend/src/components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue
create mode 100644 packages/app-lib/src/api/instance/icon.rs
diff --git a/Cargo.lock b/Cargo.lock
index 726ebeeed2..b3eefef689 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4806,7 +4806,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
- "socket2 0.5.10",
+ "socket2 0.6.5",
"system-configuration",
"tokio",
"tower-service",
@@ -8016,7 +8016,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls 0.23.32",
- "socket2 0.5.10",
+ "socket2 0.6.5",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -8053,7 +8053,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
- "socket2 0.5.10",
+ "socket2 0.6.5",
"tracing",
"windows-sys 0.60.2",
]
@@ -11020,6 +11020,7 @@ dependencies = [
"heck 0.5.0",
"hickory-resolver 0.25.2",
"httpdate",
+ "image",
"indicatif",
"itertools 0.14.0",
"modrinth-content-management",
diff --git a/apps/app-frontend/src/components/ui/instance-page-header/index.vue b/apps/app-frontend/src/components/ui/instance-page-header/index.vue
index 1cb84df1ba..59eea07309 100644
--- a/apps/app-frontend/src/components/ui/instance-page-header/index.vue
+++ b/apps/app-frontend/src/components/ui/instance-page-header/index.vue
@@ -15,12 +15,12 @@
Shared
+
diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/index.vue b/apps/app-frontend/src/components/ui/instance/instance-admonitions/index.vue
index 4d73ef6886..e16171bc5b 100644
--- a/apps/app-frontend/src/components/ui/instance/instance-admonitions/index.vue
+++ b/apps/app-frontend/src/components/ui/instance/instance-admonitions/index.vue
@@ -6,6 +6,11 @@
:instance="instance"
@published="emit('published')"
/>
+
()
const emit = defineEmits<{
published: []
delete: []
+ 'review-update': [event: MouseEvent]
}>()
const sharedInstanceWrongAccount = computed(() => props.sharedInstanceWrongAccount ?? false)
@@ -68,6 +76,14 @@ const showSharedInstancePublishAdmonition = computed(
props.instance.shared_instance?.role === 'owner' &&
props.instance.shared_instance.status === 'stale',
)
+const showSharedInstanceUpdateAdmonition = computed(
+ () =>
+ !sharedInstanceWrongAccount.value &&
+ !displayedSharedInstanceUnavailableReason.value &&
+ props.instance.install_stage === 'installed' &&
+ props.sharedInstanceRole === 'member' &&
+ props.sharedInstanceUpdateAvailable === true,
+)
const stackItems = computed(() => {
const items: InstanceAdmonitionItem[] = []
@@ -104,6 +120,15 @@ const stackItems = computed(() => {
})
}
+ if (showSharedInstanceUpdateAdmonition.value) {
+ items.push({
+ id: 'shared-instance-update-available',
+ type: 'info',
+ dismissible: false,
+ kind: 'shared-instance-update-available',
+ })
+ }
+
return items
})
diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-messages.ts b/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-messages.ts
index 5191ae9b26..011a57acaf 100644
--- a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-messages.ts
+++ b/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-messages.ts
@@ -21,6 +21,19 @@ export const instanceAdmonitionsMessages = defineMessages({
id: 'app.instance.admonitions.shared-instance.reviewing-button',
defaultMessage: 'Reviewing...',
},
+ sharedInstanceUpdateAvailableHeader: {
+ id: 'app.instance.admonitions.shared-instance.update-available-header',
+ defaultMessage: 'An update is available',
+ },
+ sharedInstanceUpdateAvailableBody: {
+ id: 'app.instance.admonitions.shared-instance.update-available-body',
+ defaultMessage:
+ 'An update is required to play {name}. Please update to latest version to launch the game.',
+ },
+ sharedInstanceReviewUpdateButton: {
+ id: 'app.instance.admonitions.shared-instance.review-update-button',
+ defaultMessage: 'Review update',
+ },
sharedInstanceReviewHeader: {
id: 'app.instance.admonitions.shared-instance.review-header',
defaultMessage: 'Review changes',
diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-update-available.vue b/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-update-available.vue
new file mode 100644
index 0000000000..ffb96f4329
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-update-available.vue
@@ -0,0 +1,34 @@
+
+
+ {{ formatMessage(messages.sharedInstanceUpdateAvailableBody, { name: instanceName }) }}
+
+
+
+
+ {{ formatMessage(messages.sharedInstanceReviewUpdateButton) }}
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/types.ts b/apps/app-frontend/src/components/ui/instance/instance-admonitions/types.ts
index ea7eb81be1..02948b3e8d 100644
--- a/apps/app-frontend/src/components/ui/instance/instance-admonitions/types.ts
+++ b/apps/app-frontend/src/components/ui/instance/instance-admonitions/types.ts
@@ -2,6 +2,7 @@ import type { StackedAdmonitionItem } from '@modrinth/ui'
export type InstanceAdmonitionKind =
| 'shared-instance-stale'
+ | 'shared-instance-update-available'
| 'shared-instance-unavailable'
| 'shared-instance-wrong-account'
diff --git a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
index 3c84d90668..8568ebefa6 100644
--- a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
+++ b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
@@ -109,9 +109,13 @@ watch(selectedReleaseChannel, async (channel, previousChannel) => {
})
async function resetIcon() {
- icon.value = undefined
- await edit_icon(instance.value.id, null).catch(handleError)
- trackEvent('InstanceRemoveIcon')
+ try {
+ await edit_icon(instance.value.id, null)
+ icon.value = undefined
+ trackEvent('InstanceRemoveIcon')
+ } catch (error) {
+ handleError(error)
+ }
}
async function setIcon() {
@@ -127,10 +131,13 @@ async function setIcon() {
if (!value) return
- icon.value = value
- await edit_icon(instance.value.id, icon.value).catch(handleError)
-
- trackEvent('InstanceSetIcon')
+ try {
+ await edit_icon(instance.value.id, value)
+ icon.value = value
+ trackEvent('InstanceSetIcon')
+ } catch (error) {
+ handleError(error)
+ }
}
const editInstanceObject = computed(() => ({
diff --git a/apps/app-frontend/src/components/ui/instance_settings/SharingSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/SharingSettings.vue
index aef391eea7..0886c82e3c 100644
--- a/apps/app-frontend/src/components/ui/instance_settings/SharingSettings.vue
+++ b/apps/app-frontend/src/components/ui/instance_settings/SharingSettings.vue
@@ -1,31 +1,156 @@
-
+
+
+
+
+ {{ formatMessage(messages.activeInvitesTitle) }}
+
+
+ {{ formatMessage(messages.activeInvitesDescription) }}
+
+
+
+
+
+
+ {{ formatMessage(messages.noActiveInvites) }}
+
+
+
+
+
+
+ {{ row.uses }}
+ / {{ row.maxUses }}
+
+
+
+ {{ formatRelativeTime(row.expiration) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue b/apps/app-frontend/src/components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue
new file mode 100644
index 0000000000..e373ce7b93
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(commonMessages.cancelButton) }}
+
+
+
+
+
+ {{ formatMessage(messages.revokeButton) }}
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/shared-instances/SharedInstanceUpdateModal.vue b/apps/app-frontend/src/components/ui/shared-instances/SharedInstanceUpdateModal.vue
index f9c531fad1..342d900d56 100644
--- a/apps/app-frontend/src/components/ui/shared-instances/SharedInstanceUpdateModal.vue
+++ b/apps/app-frontend/src/components/ui/shared-instances/SharedInstanceUpdateModal.vue
@@ -43,8 +43,9 @@ import type { GameInstance } from '@/helpers/types'
type UpdateCompleteCallback = () => void | Promise
const emit = defineEmits<{
+ accepted: []
cancel: []
- complete: []
+ complete: [successful: boolean]
report: [event?: MouseEvent]
sharedInstanceUnavailable: [reason: SharedInstanceUnavailableReason | null]
}>()
@@ -72,11 +73,14 @@ const diffs = computed(
)
async function update() {
+ let successful = false
+ emit('accepted')
try {
if (instance.value) {
const job = await install_update_shared_instance(instance.value.id)
await wait_for_install_job(job.job_id)
await onComplete.value()
+ successful = true
}
} catch (error) {
if (isSharedInstanceUnavailableError(error)) {
@@ -85,7 +89,7 @@ async function update() {
}
notifySharedInstanceError(error)
} finally {
- emit('complete')
+ emit('complete', successful)
}
}
diff --git a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue
index e4ebfd949f..19a498942b 100644
--- a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue
+++ b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue
@@ -64,7 +64,29 @@
- {{ formatMessage(messages.inviteWarning) }}
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.inviteWarning) }}
+
>()
const contentModal = ref>()
const externalFileTable = ref(null)
const preview = ref(null)
+const creator = ref(null)
const install = ref<() => void | Promise>(() => {})
const reportMode = ref(false)
const reportOnly = ref(false)
@@ -302,6 +332,12 @@ const reportReasonOptions = computed[]>(() => [
const canSubmitReport = computed(
() => Boolean(preview.value && additionalContext.value.trim()) && !submitLoading.value,
)
+const creatorProfileLink = computed(() => {
+ const username = creator.value?.username
+ return username
+ ? () => openUrl(`${config.siteUrl}/user/${encodeURIComponent(username)}`)
+ : undefined
+})
async function accept() {
hide()
@@ -381,6 +417,7 @@ function handleCancel() {
}
function handleHide() {
resetReportState()
+ creator.value = null
show_ads_window()
}
function resetReportState() {
@@ -395,14 +432,17 @@ function resetReportState() {
function show(
previewValue: SharedInstanceInstallPreview,
installValue: () => void | Promise,
+ creatorValue?: SharedInstanceCreator,
event?: MouseEvent,
) {
resetReportState()
+ creator.value = creatorValue ?? null
install.value = installValue
showPreview(previewValue, event)
}
function showReport(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) {
resetReportState()
+ creator.value = null
reportMode.value = true
reportOnly.value = true
install.value = () => {}
@@ -437,6 +477,11 @@ const messages = defineMessages({
defaultMessage:
'This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust.',
},
+ inviteWarningWithCreator: {
+ id: 'app.modal.install-to-play.invite-warning-with-creator',
+ defaultMessage:
+ 'This invite was created by {username} , not Modrinth. Only accept invites from people you trust.',
+ },
reportDescription: {
id: 'app.modal.install-to-play.report-description',
defaultMessage:
diff --git a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/use-shared-instance-preview-content.ts b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/use-shared-instance-preview-content.ts
index 671b4b1899..3646f87592 100644
--- a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/use-shared-instance-preview-content.ts
+++ b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/use-shared-instance-preview-content.ts
@@ -1,7 +1,7 @@
import type { Labrinth } from '@modrinth/api-client'
import type { ContentItem } from '@modrinth/ui'
-import { get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
+import { get_project, get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
import type { SharedInstanceInstallPreview } from '@/helpers/install'
type VersionDependency = Labrinth.Versions.v2.Dependency & { version_id?: string }
@@ -20,7 +20,18 @@ export function useSharedInstancePreviewContent() {
async function modpackContentItems(preview: SharedInstanceInstallPreview) {
if (!preview.modpackVersionId) return []
const version = await get_version(preview.modpackVersionId, 'must_revalidate')
- return await contentItemsFromDependencies(version?.dependencies ?? [])
+ if (!version) return []
+
+ const [project, contentItems] = await Promise.all([
+ get_project(version.project_id, 'must_revalidate'),
+ contentItemsFromDependencies(version.dependencies ?? []),
+ ])
+ if (!project) return contentItems
+
+ return contentItems.map((item) => ({
+ ...item,
+ source: { project },
+ }))
}
async function contentItemsFromDependencies(dependencies: Labrinth.Versions.v2.Dependency[]) {
diff --git a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-invite-handler/use-shared-instance-invite-handler.ts b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-invite-handler/use-shared-instance-invite-handler.ts
index c71fe78f27..97d197dbcc 100644
--- a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-invite-handler/use-shared-instance-invite-handler.ts
+++ b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-invite-handler/use-shared-instance-invite-handler.ts
@@ -17,6 +17,7 @@ import {
install_shared_instance,
} from '@/helpers/install'
import { list } from '@/helpers/instance'
+import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
import { useTheming } from '@/store/state'
import { parseSharedInstanceInviteNotification } from './shared-instance-invite-parser'
@@ -26,9 +27,15 @@ type InstallModal = {
show(
preview: Awaited>,
install: () => Promise,
+ creator?: SharedInstanceCreator,
): void
}
+type SharedInstanceCreator = {
+ username: string
+ avatarUrl: string | null
+}
+
type AccountRequiredModal = {
show(event?: MouseEvent): Promise
}
@@ -45,6 +52,8 @@ export function useSharedInstanceInviteHandler(
const auth = injectAuth()
const client = injectModrinthClient()
const { handleError } = injectNotificationManager()
+ const { notifySharedInstanceConnectionError, notifySharedInstanceError } =
+ useSharedInstanceErrors()
const popupNotificationManager = injectPopupNotificationManager()
const queryClient = useQueryClient()
const router = useRouter()
@@ -58,6 +67,7 @@ export function useSharedInstanceInviteHandler(
instanceId: string
preview: Awaited>
install: () => Promise
+ creator?: SharedInstanceCreator
onGoToInstance?: () => void | Promise
}
| undefined
@@ -73,10 +83,13 @@ export function useSharedInstanceInviteHandler(
async function resolveInvite(invite: SharedInstanceInvite) {
const [invitedBy, sharedInstance] = await Promise.all([
- !invite.invitedByUsername && invite.invitedById
+ (!invite.invitedByUsername || !invite.invitedByAvatarUrl) && invite.invitedById
? get_user(invite.invitedById, 'bypass').catch(() => null)
: null,
- client.sharedinstances.instances_v1.get(invite.sharedInstanceId).catch(() => null),
+ client.sharedinstances.instances_v1.get(invite.sharedInstanceId).catch(() => {
+ notifySharedInstanceConnectionError()
+ return null
+ }),
])
return {
@@ -90,15 +103,17 @@ export function useSharedInstanceInviteHandler(
function showInstall(
preview: Awaited>,
install: () => Promise,
+ creator?: SharedInstanceCreator,
) {
if (!installModal.value) throw new Error('Shared instance install modal is not available.')
- installModal.value.show(preview, install)
+ installModal.value.show(preview, install, creator)
}
async function showInstallOrAlreadyInstalled(
sharedInstanceId: string,
preview: Awaited>,
install: () => Promise,
+ creator?: SharedInstanceCreator,
onGoToInstance?: () => void | Promise,
) {
const existingInstance = (await list()).find(
@@ -106,7 +121,7 @@ export function useSharedInstanceInviteHandler(
)
if (!existingInstance || themeStore.getFeatureFlag('skip_non_essential_warnings')) {
- showInstall(preview, install)
+ showInstall(preview, install, creator)
return
}
@@ -118,6 +133,7 @@ export function useSharedInstanceInviteHandler(
instanceId: existingInstance.id,
preview,
install,
+ creator,
onGoToInstance,
}
alreadyInstalledModal.value.show(existingInstance.name)
@@ -146,7 +162,7 @@ export function useSharedInstanceInviteHandler(
const pending = pendingAlreadyInstalled
pendingAlreadyInstalled = undefined
if (!pending) return
- showInstall(pending.preview, pending.install)
+ showInstall(pending.preview, pending.install, pending.creator)
}
async function acceptNotification(notification: AppNotification, invite: SharedInstanceInvite) {
@@ -172,10 +188,16 @@ export function useSharedInstanceInviteHandler(
await markNotificationRead(notification)
await queryClient.invalidateQueries({ queryKey: ['instances'] })
},
+ invite.invitedByUsername
+ ? {
+ username: invite.invitedByUsername,
+ avatarUrl: invite.invitedByAvatarUrl,
+ }
+ : undefined,
() => markNotificationRead(notification),
)
} catch (error) {
- handleError(toError(error))
+ notifySharedInstanceError(error)
}
}
@@ -253,19 +275,32 @@ export function useSharedInstanceInviteHandler(
try {
if (!(await requireAccount())) return
const invite = await install_accept_shared_instance_invite(inviteId)
- await showInstallOrAlreadyInstalled(invite.sharedInstanceId, invite.preview, async () => {
- await install_shared_instance(
- invite.sharedInstanceId,
- invite.preview.name,
- invite.managerId,
- invite.serverManagerName,
- invite.serverManagerIconUrl,
- invite.instanceIconUrl,
- )
- await queryClient.invalidateQueries({ queryKey: ['instances'] })
- })
+ const manager = invite.managerId
+ ? await get_user(invite.managerId, 'bypass').catch(() => null)
+ : null
+ await showInstallOrAlreadyInstalled(
+ invite.sharedInstanceId,
+ invite.preview,
+ async () => {
+ await install_shared_instance(
+ invite.sharedInstanceId,
+ invite.preview.name,
+ invite.managerId,
+ invite.serverManagerName,
+ invite.serverManagerIconUrl,
+ invite.instanceIconUrl,
+ )
+ await queryClient.invalidateQueries({ queryKey: ['instances'] })
+ },
+ manager
+ ? {
+ username: manager.username,
+ avatarUrl: manager.avatar_url ?? null,
+ }
+ : undefined,
+ )
} catch (error) {
- handleError(toError(error))
+ notifySharedInstanceError(error)
}
}
diff --git a/apps/app-frontend/src/helpers/install.ts b/apps/app-frontend/src/helpers/install.ts
index 69d608fd7e..6c92aaa897 100644
--- a/apps/app-frontend/src/helpers/install.ts
+++ b/apps/app-frontend/src/helpers/install.ts
@@ -102,6 +102,7 @@ export interface SharedInstanceUpdateDiff {
}
export const SHARED_INSTANCE_UNAVAILABLE_ERROR_CODE = 'shared_instance_unavailable'
+export const SHARED_INSTANCES_API_ERROR_CODE = 'shared_instances_api_error'
export type SharedInstanceUnavailableReason = 'deleted' | 'access_revoked' | 'quarantined'
@@ -113,6 +114,10 @@ export function isSharedInstanceUnavailableError(error: unknown) {
return getSharedInstanceUnavailableReason(error) !== null
}
+export function isSharedInstancesApiError(error: unknown) {
+ return isRecord(error) && error.code === SHARED_INSTANCES_API_ERROR_CODE
+}
+
export function getSharedInstanceUnavailableReason(
error: unknown,
): SharedInstanceUnavailableReason | null {
diff --git a/apps/app-frontend/src/helpers/instance.ts b/apps/app-frontend/src/helpers/instance.ts
index caf54aa46b..512af7cf40 100644
--- a/apps/app-frontend/src/helpers/instance.ts
+++ b/apps/app-frontend/src/helpers/instance.ts
@@ -365,6 +365,13 @@ export interface SharedInstanceInviteLink {
maxUses: number
}
+export interface SharedInstanceInvite {
+ id: string
+ expiration: string
+ maxUses: number
+ uses: number
+}
+
export async function can_current_user_use_shared_instances(): Promise {
return await invoke('plugin:instance|instance_share_can_current_user_use')
}
@@ -394,6 +401,19 @@ export async function create_shared_instance_invite_link(
})
}
+export async function get_shared_instance_invites(
+ instanceId: string,
+): Promise {
+ return await invoke('plugin:instance|instance_share_get_invites', { instanceId })
+}
+
+export async function revoke_shared_instance_invite(
+ instanceId: string,
+ inviteId: string,
+): Promise {
+ return await invoke('plugin:instance|instance_share_revoke_invite', { instanceId, inviteId })
+}
+
export async function remove_shared_instance_users(
instanceId: string,
userIds: string[],
diff --git a/apps/app-frontend/src/helpers/shared-instance-errors.ts b/apps/app-frontend/src/helpers/shared-instance-errors.ts
index e1e92efcae..b0ce5d0f74 100644
--- a/apps/app-frontend/src/helpers/shared-instance-errors.ts
+++ b/apps/app-frontend/src/helpers/shared-instance-errors.ts
@@ -1,6 +1,10 @@
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
-import { getErrorMessage, type SharedInstanceUnavailableReason } from '@/helpers/install'
+import {
+ getErrorMessage,
+ isSharedInstancesApiError,
+ type SharedInstanceUnavailableReason,
+} from '@/helpers/install'
export const sharedInstanceErrorMessages = defineMessages({
unavailableTitle: {
@@ -39,6 +43,14 @@ export const sharedInstanceErrorMessages = defineMessages({
id: 'instance.shared-instance.error.title',
defaultMessage: 'Something has gone wrong',
},
+ networkErrorTitle: {
+ id: 'instance.shared-instance.network-error.title',
+ defaultMessage: 'Network error',
+ },
+ networkErrorText: {
+ id: 'instance.shared-instance.network-error.text',
+ defaultMessage: 'Unable to connect to shared instances API',
+ },
})
export function sharedInstanceUnavailableTextMessage(
@@ -82,7 +94,20 @@ export function useSharedInstanceErrors() {
})
}
+ function notifySharedInstanceConnectionError() {
+ addNotification({
+ type: 'error',
+ title: formatMessage(sharedInstanceErrorMessages.networkErrorTitle),
+ text: formatMessage(sharedInstanceErrorMessages.networkErrorText),
+ })
+ }
+
function notifySharedInstanceError(error: unknown) {
+ if (isSharedInstancesApiError(error)) {
+ notifySharedInstanceConnectionError()
+ return
+ }
+
addNotification({
type: 'error',
title: formatMessage(sharedInstanceErrorMessages.errorTitle),
@@ -92,6 +117,7 @@ export function useSharedInstanceErrors() {
return {
formatSharedInstanceUnavailable,
+ notifySharedInstanceConnectionError,
notifySharedInstanceError,
notifySharedInstanceUnavailable,
}
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index 165832234d..e85db4574d 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -374,9 +374,18 @@
"app.instance.admonitions.shared-instance.review-header": {
"message": "Review changes"
},
+ "app.instance.admonitions.shared-instance.review-update-button": {
+ "message": "Review update"
+ },
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Reviewing..."
},
+ "app.instance.admonitions.shared-instance.update-available-body": {
+ "message": "An update is required to play {name}. Please update to latest version to launch the game."
+ },
+ "app.instance.admonitions.shared-instance.update-available-header": {
+ "message": "An update is available"
+ },
"app.instance.confirm-delete.admonition-body": {
"message": "All data for your instance will be permanently deleted, including your worlds, configs, and all installed content."
},
@@ -443,6 +452,9 @@
"app.instance.share.invite-modal.heading": {
"message": "Share {name}"
},
+ "app.instance.share.invite-modal.user-limit-reached": {
+ "message": "This instance has reached the {limit}-user limit."
+ },
"app.instance.share.locked.empty-description-prefix": {
"message": "You need to sign in as"
},
@@ -461,6 +473,12 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Wrong account"
},
+ "app.instance.share.members.empty": {
+ "message": "No users have joined yet"
+ },
+ "app.instance.share.members.no-filter-results": {
+ "message": "No users match your filters."
+ },
"app.instance.share.remove-user-modal.effect-access": {
"message": "They will no longer receive updates for this shared instance"
},
@@ -491,6 +509,12 @@
"app.instance.share.sign-in.button": {
"message": "Sign in"
},
+ "app.instance.share.unable-to-connect.description": {
+ "message": "The shared instances service is not accessible at the moment, please try again later"
+ },
+ "app.instance.share.unable-to-connect.heading": {
+ "message": "Unable to connect"
+ },
"app.instance.share.unlink.body": {
"message": "You must unlink this modpack to share your instance"
},
@@ -650,6 +674,9 @@
"app.modal.install-to-play.invite-warning": {
"message": "This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust."
},
+ "app.modal.install-to-play.invite-warning-with-creator": {
+ "message": "This invite was created by {username} , not Modrinth. Only accept invites from people you trust."
+ },
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# mods}}"
},
@@ -1376,6 +1403,45 @@
"instance.server-modal.resource-pack": {
"message": "Resource pack"
},
+ "instance.settings.sharing.active-invites.actions": {
+ "message": "Actions"
+ },
+ "instance.settings.sharing.active-invites.code": {
+ "message": "Invite link"
+ },
+ "instance.settings.sharing.active-invites.description": {
+ "message": "Anyone with one of these invite links can join while it remains active."
+ },
+ "instance.settings.sharing.active-invites.empty": {
+ "message": "There are no active invites."
+ },
+ "instance.settings.sharing.active-invites.expires": {
+ "message": "Expires"
+ },
+ "instance.settings.sharing.active-invites.revoke": {
+ "message": "Revoke invite"
+ },
+ "instance.settings.sharing.active-invites.revoke-with-code": {
+ "message": "Revoke invite {code}"
+ },
+ "instance.settings.sharing.active-invites.title": {
+ "message": "Active invites"
+ },
+ "instance.settings.sharing.active-invites.uses": {
+ "message": "Uses"
+ },
+ "instance.settings.sharing.revoke-invite.admonition-body": {
+ "message": "The invite link {code} will stop working immediately. People who already joined will keep access."
+ },
+ "instance.settings.sharing.revoke-invite.admonition-header": {
+ "message": "This action cannot be undone"
+ },
+ "instance.settings.sharing.revoke-invite.confirm": {
+ "message": "Revoke invite"
+ },
+ "instance.settings.sharing.revoke-invite.header": {
+ "message": "Revoke invite"
+ },
"instance.settings.tabs.general": {
"message": "General"
},
@@ -1577,6 +1643,12 @@
"instance.shared-instance.error.title": {
"message": "Something has gone wrong"
},
+ "instance.shared-instance.network-error.text": {
+ "message": "Unable to connect to shared instances API"
+ },
+ "instance.shared-instance.network-error.title": {
+ "message": "Network error"
+ },
"instance.shared-instance.owner-tooltip": {
"message": "This instance's content is being shared to other users."
},
diff --git a/apps/app-frontend/src/pages/instance/Index.vue b/apps/app-frontend/src/pages/instance/Index.vue
index 8d937f2f90..9d3f10200d 100644
--- a/apps/app-frontend/src/pages/instance/Index.vue
+++ b/apps/app-frontend/src/pages/instance/Index.vue
@@ -16,6 +16,8 @@
reportSharedInstance(event, true)"
/>
@@ -62,8 +64,10 @@
:shared-instance-expected-user-id="sharedInstanceExpectedUserId"
:shared-instance-role="instance.shared_instance?.role"
:shared-instance-signed-out="sharedInstanceSignedOut"
+ :shared-instance-update-available="showSharedInstanceUpdateAdmonition"
@published="fetchInstance"
@delete="requestInstanceDeletion"
+ @review-update="reviewSharedInstanceUpdate"
/>
@@ -223,6 +227,7 @@ const sharedInstanceUpdateModal = ref
>()
const deleteConfirmModal = ref>()
const selectedInstanceToDelete = ref(null)
+const hiddenSharedInstanceUpdateKey = ref(null)
const { notifySharedInstanceError, notifySharedInstanceUnavailable } = useSharedInstanceErrors()
@@ -253,8 +258,19 @@ const {
signedOut: sharedInstanceSignedOut,
unavailableManager: sharedInstanceUnavailableManager,
unavailableReason: sharedInstanceUnavailableReason,
+ updatePreview: sharedInstanceUpdatePreview,
wrongAccount: sharedInstanceWrongAccount,
} = sharedInstanceState
+const sharedInstanceUpdateKey = computed(() => {
+ const instanceId = instance.value?.id
+ const latestVersion = sharedInstanceUpdatePreview.value?.latestVersion
+ return instanceId && latestVersion !== undefined ? `${instanceId}:${latestVersion}` : null
+})
+const showSharedInstanceUpdateAdmonition = computed(
+ () =>
+ sharedInstanceUpdatePreview.value?.updateAvailable === true &&
+ sharedInstanceUpdateKey.value !== hiddenSharedInstanceUpdateKey.value,
+)
watch(
() => router.currentRoute.value,
@@ -530,6 +546,37 @@ async function handleSharedInstanceUnavailable(
setSharedInstanceUnavailable(reason)
}
+function reviewSharedInstanceUpdate(event: MouseEvent) {
+ const currentInstance = instance.value
+ const preview = sharedInstanceUpdatePreview.value
+ if (
+ !currentInstance ||
+ currentInstance.shared_instance?.role !== 'member' ||
+ !preview?.updateAvailable
+ ) {
+ return
+ }
+
+ sharedInstanceUpdateModal.value?.show(
+ currentInstance,
+ preview,
+ async () => {
+ await fetchInstance()
+ },
+ event,
+ )
+}
+
+function hideAcceptedSharedInstanceUpdate() {
+ hiddenSharedInstanceUpdateKey.value = sharedInstanceUpdateKey.value
+}
+
+function handleSharedInstanceUpdateComplete(successful: boolean) {
+ if (!successful && hiddenSharedInstanceUpdateKey.value === sharedInstanceUpdateKey.value) {
+ hiddenSharedInstanceUpdateKey.value = null
+ }
+}
+
const startInstance = async (context: string) => {
if (!instance.value || instance.value.quarantined) return
if (checkingSharedInstanceLaunch.value || loading.value || playing.value) return
@@ -540,17 +587,16 @@ const startInstance = async (context: string) => {
!!instance.value.shared_instance && !sharedInstanceActionsLocked.value && !offline.value
if (canCheckSharedInstanceUpdate) {
- let preview: Awaited>
+ let preview: Awaited> = null
checkingSharedInstanceLaunch.value = true
try {
preview = await refreshSharedInstanceUpdatePreview()
} catch (error) {
if (isSharedInstanceUnavailableError(error)) {
await handleSharedInstanceUnavailable(getSharedInstanceUnavailableReason(error))
- } else {
- notifySharedInstanceError(error)
+ return
}
- return
+ notifySharedInstanceError(error)
} finally {
checkingSharedInstanceLaunch.value = false
}
@@ -657,7 +703,7 @@ async function reportSharedInstance(event?: MouseEvent, closeUpdateModal = false
if (closeUpdateModal) sharedInstanceUpdateModal.value?.hide()
sharedInstanceReportModal.value?.showReport(preview, event)
} catch (error) {
- handleError(error as Error)
+ notifySharedInstanceError(error)
}
}
diff --git a/apps/app-frontend/src/pages/instance/share/index.vue b/apps/app-frontend/src/pages/instance/share/index.vue
index 10ba2b04e0..b6addbe3bf 100644
--- a/apps/app-frontend/src/pages/instance/share/index.vue
+++ b/apps/app-frontend/src/pages/instance/share/index.vue
@@ -9,9 +9,19 @@
:link="inviteLink.link.value"
:link-expires-at="inviteLink.details.value?.expiresAt"
:link-max-uses="inviteLink.details.value?.maxUses"
+ :link-max-uses-limit="remainingUserSlots"
:update-invite-link="inviteLink.update"
:user-profile-link="userProfileLink"
- :can-invite="!members.exclusiveMutationPending.value && !inviteLink.pending.value"
+ :can-invite="
+ hasRemainingUserSlots &&
+ !members.exclusiveMutationPending.value &&
+ !inviteLink.pending.value
+ "
+ :invite-disabled-message="
+ hasRemainingUserSlots
+ ? undefined
+ : formatMessage(messages.userLimitReached, { limit: SHARED_INSTANCE_USER_LIMIT })
+ "
@invite="invitePlayer"
@cancel="cancelInvite"
/>
@@ -37,10 +47,19 @@
@state-change="publishState = $event"
/>
+
+
+
+
auth.user.value?.id ?? null)
const isSignedIn = computed(() => !!auth.session_token.value)
+const sharedInstancesApiUnavailable = ref(false)
const accountRequiredModal = ref>()
const invitePlayersModal = ref>()
const unlinkModal = ref>()
@@ -198,10 +219,22 @@ function notifyOperationError(error: unknown) {
sharedInstanceState.unavailableManager.value,
)
} else {
+ if (isSharedInstancesApiError(error)) sharedInstancesApiUnavailable.value = true
notifySharedInstanceError(error)
}
}
+const eligibilityQuery = useQuery({
+ queryKey: computed(() => ['shared-instance-eligibility', currentUserId.value]),
+ queryFn: can_current_user_use_shared_instances,
+ enabled: () => isSignedIn.value && !!currentUserId.value,
+ retry: false,
+ staleTime: Infinity,
+ refetchOnMount: 'always',
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+})
+
const members = useSharedInstanceMembers({
instance,
currentUserId,
@@ -209,6 +242,10 @@ const members = useSharedInstanceMembers({
actionsLocked,
onError: notifyOperationError,
})
+const remainingUserSlots = computed(() =>
+ Math.max(0, SHARED_INSTANCE_USER_LIMIT - members.rows.value.length),
+)
+const hasRemainingUserSlots = computed(() => remainingUserSlots.value > 0)
const {
inviteFriends,
search: searchInviteUsers,
@@ -221,6 +258,7 @@ const {
})
const inviteLink = useSharedInstanceInviteLink(
computed(() => props.instance.id),
+ remainingUserSlots,
notifyOperationError,
)
@@ -239,6 +277,29 @@ const lockedActionButton = computed(() =>
const sharedInstanceUnavailableReason = sharedInstanceState.unavailableReason
const sharedInstanceUnavailable = computed(() => !!sharedInstanceUnavailableReason.value)
const sharedInstanceUnavailableManager = sharedInstanceState.unavailableManager
+const unableToConnect = computed(
+ () =>
+ sharedInstancesApiUnavailable.value ||
+ isSharedInstancesApiError(eligibilityQuery.error.value) ||
+ isSharedInstancesApiError(members.query.error.value),
+)
+const membersTableLoading = computed(
+ () =>
+ members.rows.value.length === 0 &&
+ !!props.instance.shared_instance &&
+ (members.query.data.value === undefined || members.query.isFetching.value) &&
+ !sharedInstanceUnavailable.value &&
+ !sharedInstanceActionsLocked.value,
+)
+const showMembersTable = computed(
+ () =>
+ members.rows.value.length > 0 ||
+ (!!props.instance.shared_instance &&
+ members.query.data.value !== undefined &&
+ !members.query.isFetching.value &&
+ !sharedInstanceUnavailable.value &&
+ !sharedInstanceActionsLocked.value),
+)
const requiresUnlink = computed(
() =>
props.instance.link?.type === 'imported_modpack' &&
@@ -253,6 +314,15 @@ const importedModpackBackupTip = computed(() =>
const messages = defineMessages({
signInButton: { id: 'app.instance.share.sign-in.button', defaultMessage: 'Sign in' },
+ unableToConnectHeading: {
+ id: 'app.instance.share.unable-to-connect.heading',
+ defaultMessage: 'Unable to connect',
+ },
+ unableToConnectDescription: {
+ id: 'app.instance.share.unable-to-connect.description',
+ defaultMessage:
+ 'The shared instances service is not accessible at the moment, please try again later',
+ },
noFriendsInvitedHeading: {
id: 'app.instance.share.empty.heading',
defaultMessage: 'No friends invited',
@@ -265,6 +335,10 @@ const messages = defineMessages({
id: 'app.instance.share.empty.invite-friends-button',
defaultMessage: 'Invite friends',
},
+ userLimitReached: {
+ id: 'app.instance.share.invite-modal.user-limit-reached',
+ defaultMessage: 'This instance has reached the {limit}-user limit.',
+ },
shareModalHeader: {
id: 'app.instance.share.invite-modal.heading',
defaultMessage: 'Share {name}',
@@ -304,7 +378,7 @@ const messages = defineMessages({
})
function invitePlayer(payload: InvitePlayersInvitePayload) {
- if (actionsLocked.value) return
+ if (actionsLocked.value || !hasRemainingUserSlots.value) return
if (payload.source === 'search') void requestFriend(payload.user)
members.invite(payload.user)
}
@@ -315,6 +389,7 @@ function cancelInvite(user: InvitePlayersUser) {
async function showInvitePlayers(event?: MouseEvent) {
if (actionsLocked.value) return
if (!isSignedIn.value) return signInToShare(event)
+ if (!hasRemainingUserSlots.value) return
if (requiresUnlink.value) return unlinkModal.value?.show()
if (await inviteLink.ensure()) invitePlayersModal.value?.show(event)
}
@@ -353,6 +428,20 @@ function signInToShare(event?: MouseEvent) {
void accountRequiredModal.value?.show(event)
}
+watch(
+ [eligibilityQuery.error, members.query.error],
+ (errors) => {
+ for (const error of errors) {
+ if (isSharedInstancesApiError(error)) notifyOperationError(error)
+ }
+ },
+ { immediate: true },
+)
+watch([eligibilityQuery.data, members.query.data], ([eligibility, memberRows]) => {
+ if (eligibility !== undefined && memberRows !== undefined) {
+ sharedInstancesApiUnavailable.value = false
+ }
+})
watch(
() => props.instance.id,
() => {
diff --git a/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue b/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue
index 85abb14955..85cc02c8c2 100644
--- a/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue
+++ b/apps/app-frontend/src/pages/instance/share/shared-instance-members-table.vue
@@ -25,7 +25,7 @@
@@ -35,7 +35,7 @@
-
+
- No users match your filters.
+ {{
+ formatMessage(rows.length === 0 ? messages.noUsersJoined : messages.noUsersMatchFilters)
+ }}
@@ -157,7 +159,7 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
-import { computed, ref } from 'vue'
+import { computed, ref, watch } from 'vue'
import {
type MethodFilter,
@@ -170,6 +172,7 @@ import {
const props = defineProps<{
rows: ShareRow[]
actionsLocked?: boolean
+ inviteDisabled?: boolean
invitePending?: boolean
pushUpdateDisabled?: boolean
pushUpdatePending?: boolean
@@ -191,6 +194,7 @@ const methodFilterOptions: Array<{ id: ShareMethod; label: string }> = [
{ id: 'direct', label: methodLabels.direct },
{ id: 'link', label: methodLabels.link },
]
+const hasMultipleMethods = computed(() => new Set(props.rows.map((row) => row.method)).size > 1)
const columns = computed[]>(() => {
const result: TableColumn[] = [
{
@@ -286,11 +290,23 @@ function filterClass(active: boolean) {
]
}
+watch(hasMultipleMethods, (multiple) => {
+ if (!multiple) methodFilter.value = 'all'
+})
+
const messages = defineMessages({
pushUpdate: {
id: 'app.instance.admonitions.shared-instance.publish-button',
defaultMessage: 'Push update',
},
+ noUsersJoined: {
+ id: 'app.instance.share.members.empty',
+ defaultMessage: 'No users have joined yet',
+ },
+ noUsersMatchFilters: {
+ id: 'app.instance.share.members.no-filter-results',
+ defaultMessage: 'No users match your filters.',
+ },
})
function userProfileLink(username: string) {
return !username || username.includes('@') ? undefined : `/user/${encodeURIComponent(username)}`
diff --git a/apps/app-frontend/src/pages/instance/share/shared-instance-share-types.ts b/apps/app-frontend/src/pages/instance/share/shared-instance-share-types.ts
index a6e478ba4e..a9bebd01c2 100644
--- a/apps/app-frontend/src/pages/instance/share/shared-instance-share-types.ts
+++ b/apps/app-frontend/src/pages/instance/share/shared-instance-share-types.ts
@@ -2,6 +2,8 @@ export type ShareMethod = 'direct' | 'link'
export type MethodFilter = ShareMethod | 'all'
export type ShareTableColumn = 'username' | 'lastPlayed' | 'joined' | 'method' | 'actions'
+export const SHARED_INSTANCE_USER_LIMIT = 50
+
export type ShareRow = {
id: string
username: string
diff --git a/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-link.ts b/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-link.ts
index 7a7d0a77a9..d7ad386cf6 100644
--- a/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-link.ts
+++ b/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-link.ts
@@ -5,8 +5,11 @@ import { config } from '@/config'
import { toError } from '@/helpers/errors'
import { create_shared_instance_invite_link } from '@/helpers/instance'
+const DEFAULT_INVITE_LINK_MAX_USES = 10
+
export function useSharedInstanceInviteLink(
instanceId: Ref,
+ maxInviteUses: Ref,
onError: (error: unknown) => void,
) {
const details = ref>>()
@@ -20,10 +23,12 @@ export function useSharedInstanceInviteLink(
async function ensure() {
if (details.value) return true
if (pending.value) return false
+ const maxUses = Math.min(DEFAULT_INVITE_LINK_MAX_USES, Math.floor(maxInviteUses.value))
+ if (maxUses <= 0) return false
pending.value = true
try {
- details.value = await create_shared_instance_invite_link(instanceId.value)
+ details.value = await create_shared_instance_invite_link(instanceId.value, { maxUses })
return true
} catch (error) {
onError(error)
@@ -35,6 +40,8 @@ export function useSharedInstanceInviteLink(
async function update(settings: InviteLinkSettings) {
if (!details.value) return
+ const maxInviteLinkUses = Math.floor(maxInviteUses.value)
+ if (maxInviteLinkUses <= 0) return
pending.value = true
try {
@@ -44,7 +51,7 @@ export function useSharedInstanceInviteLink(
)
details.value = await create_shared_instance_invite_link(instanceId.value, {
maxAgeSeconds,
- maxUses: settings.maxUses,
+ maxUses: Math.min(settings.maxUses, maxInviteLinkUses),
replaceInviteId: details.value.inviteId,
})
} catch (error) {
diff --git a/apps/app-frontend/src/pages/instance/share/use-shared-instance-members.ts b/apps/app-frontend/src/pages/instance/share/use-shared-instance-members.ts
index 2b6d1325d0..801f9a3552 100644
--- a/apps/app-frontend/src/pages/instance/share/use-shared-instance-members.ts
+++ b/apps/app-frontend/src/pages/instance/share/use-shared-instance-members.ts
@@ -12,7 +12,11 @@ import {
} from '@/helpers/instance'
import type { GameInstance } from '@/helpers/types'
-import { normalizeInviteKey, type ShareRow } from './shared-instance-share-types'
+import {
+ normalizeInviteKey,
+ SHARED_INSTANCE_USER_LIMIT,
+ type ShareRow,
+} from './shared-instance-share-types'
type MembersQueryKey = readonly ['sharedInstanceUsers', string]
@@ -54,6 +58,7 @@ export function useSharedInstanceMembers(options: {
queryFn: ({ queryKey }) => fetchRows(queryKey),
enabled: () =>
options.isSignedIn.value && !!options.instance.value.id && !options.actionsLocked.value,
+ retry: false,
staleTime: Infinity,
refetchOnMount: 'always',
refetchOnReconnect: false,
@@ -139,6 +144,7 @@ export function useSharedInstanceMembers(options: {
if (
options.actionsLocked.value ||
exclusiveMutationPending.value ||
+ rows.value.length >= SHARED_INSTANCE_USER_LIMIT ||
invitingUserIds.has(normalizedId) ||
find(user.id, user.username)
) {
diff --git a/apps/app/build.rs b/apps/app/build.rs
index eb826b13da..514b389e19 100644
--- a/apps/app/build.rs
+++ b/apps/app/build.rs
@@ -227,6 +227,8 @@ fn main() {
"instance_share_get_users",
"instance_share_invite_users",
"instance_share_create_invite_link",
+ "instance_share_get_invites",
+ "instance_share_revoke_invite",
"instance_share_remove_users",
"instance_share_get_publish_preview",
"instance_share_publish",
diff --git a/apps/app/src/api/instance.rs b/apps/app/src/api/instance.rs
index 3de25824f9..570deca20b 100644
--- a/apps/app/src/api/instance.rs
+++ b/apps/app/src/api/instance.rs
@@ -56,6 +56,8 @@ pub fn init() -> tauri::plugin::TauriPlugin {
instance_share_get_users,
instance_share_invite_users,
instance_share_create_invite_link,
+ instance_share_get_invites,
+ instance_share_revoke_invite,
instance_share_remove_users,
instance_share_get_publish_preview,
instance_share_publish,
@@ -825,6 +827,27 @@ pub async fn instance_share_create_invite_link(
.await?)
}
+#[tauri::command]
+pub async fn instance_share_get_invites(
+ instance_id: &str,
+) -> Result> {
+ Ok(theseus::instance::get_shared_instance_invites(instance_id).await?)
+}
+
+#[tauri::command]
+pub async fn instance_share_revoke_invite(
+ instance_id: &str,
+ invite_id: String,
+) -> Result<()> {
+ Ok(
+ theseus::instance::revoke_shared_instance_invite(
+ instance_id,
+ invite_id,
+ )
+ .await?,
+ )
+}
+
#[tauri::command]
pub async fn instance_share_remove_users(
instance_id: &str,
diff --git a/apps/app/src/api/mod.rs b/apps/app/src/api/mod.rs
index acbe75b556..d7c0853ad8 100644
--- a/apps/app/src/api/mod.rs
+++ b/apps/app/src/api/mod.rs
@@ -91,14 +91,25 @@ macro_rules! impl_serialize {
theseus::ErrorKind::SharedInstanceUnavailable(reason) => Some(reason),
_ => None,
};
+ let code = match theseus_error.raw.as_ref() {
+ theseus::ErrorKind::SharedInstanceUnavailable(_) => {
+ Some("shared_instance_unavailable")
+ }
+ theseus::ErrorKind::SharedInstancesApiError(_) => {
+ Some("shared_instances_api_error")
+ }
+ _ => None,
+ };
let mut state = serializer.serialize_struct(
"Theseus",
- if unavailable_reason.is_some() { 4 } else { 2 },
+ 2 + usize::from(code.is_some()) + usize::from(unavailable_reason.is_some()),
)?;
state.serialize_field("field_name", "Theseus")?;
state.serialize_field("message", &theseus_error.to_string())?;
+ if let Some(code) = code {
+ state.serialize_field("code", code)?;
+ }
if let Some(reason) = unavailable_reason {
- state.serialize_field("code", "shared_instance_unavailable")?;
state.serialize_field("reason", reason)?;
}
state.end()
diff --git a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue
index 247ba590e3..6e2a91f743 100644
--- a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue
+++ b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue
@@ -604,13 +604,18 @@ async function loadSharedInstanceVersionContent(
dependency.project_id ? [dependency.project_id] : [],
)
const projectIds = [
- ...new Set([...versions.map((version) => version.project_id), ...dependencyProjectIds]),
+ ...new Set([
+ ...versions.map((version) => version.project_id),
+ ...dependencyProjectIds,
+ ...(modpackVersion ? [modpackVersion.project_id] : []),
+ ]),
]
const projects = projectIds.length
? await client.labrinth.projects_v2.getMultiple(projectIds)
: []
const versionsById = new Map(versions.map((version) => [version.id, version]))
const projectsById = new Map(projects.map((project) => [project.id, project]))
+ const modpackProject = modpackVersion ? projectsById.get(modpackVersion.project_id) : undefined
const directContent: ContentItem[] = [...new Set(directVersionIds)].flatMap((versionId) => {
const version = versionsById.get(versionId)
@@ -633,13 +638,19 @@ async function loadSharedInstanceVersionContent(
const fileName =
primaryFile?.filename ?? dependency.file_name ?? project?.title ?? version?.name ?? 'Unknown'
- return sharedInstanceContentItem(
+ const item = sharedInstanceContentItem(
version,
project,
fileName,
dependency.project_id ?? fileName,
!project && !version,
)
+ return modpackProject
+ ? {
+ ...item,
+ source: { project: modpackProject },
+ }
+ : item
})
const externalContent: ContentItem[] = instanceVersion.external_files.map((file, index) => ({
diff --git a/apps/frontend/src/components/ui/report/ReportInfo.vue b/apps/frontend/src/components/ui/report/ReportInfo.vue
index 3f584a0d5e..0d5ccfec21 100644
--- a/apps/frontend/src/components/ui/report/ReportInfo.vue
+++ b/apps/frontend/src/components/ui/report/ReportInfo.vue
@@ -64,11 +64,18 @@
-
+
+
-
Shared instance
+
{{ report.shared_instance?.name ?? 'Shared instance' }}
Version {{ report.shared_instance_version_id ?? 'unknown' }} ·
@@ -89,8 +96,7 @@
diff --git a/apps/frontend/src/components/ui/report/ReportView.vue b/apps/frontend/src/components/ui/report/ReportView.vue
index 7989da545c..2ed7ef236d 100644
--- a/apps/frontend/src/components/ui/report/ReportView.vue
+++ b/apps/frontend/src/components/ui/report/ReportView.vue
@@ -1,5 +1,5 @@
-
+
!!projectId.value),
})
+const sharedInstanceId = computed(() =>
+ rawReport.value?.item_type === 'shared-instance' ? rawReport.value.item_id : null,
+)
+
+const { data: sharedInstance } = useQuery({
+ queryKey: computed(() => ['shared-instance', sharedInstanceId.value]),
+ queryFn: () => client.sharedinstances.instances_v1.get(sharedInstanceId.value),
+ enabled: computed(() => !!sharedInstanceId.value),
+})
+
// Assemble the full report object
const report = computed(() => {
if (!rawReport.value) return null
@@ -118,6 +128,7 @@ const report = computed(() => {
...rawReport.value,
project: project.value ?? null,
version: version.value ?? null,
+ shared_instance: sharedInstance.value ?? null,
reporterUser: (users.value || []).find((user) => user.id === rawReport.value.reporter),
user:
rawReport.value.item_type === 'user'
diff --git a/apps/frontend/src/components/ui/report/ReportsList.vue b/apps/frontend/src/components/ui/report/ReportsList.vue
index 5f91a7132c..7e295c70cf 100644
--- a/apps/frontend/src/components/ui/report/ReportsList.vue
+++ b/apps/frontend/src/components/ui/report/ReportsList.vue
@@ -19,7 +19,7 @@
:moderation="moderation"
raised
:auth="auth"
- class="universal-card recessed"
+ class="card-shadow mb-4 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4"
/>
You don't have any active reports.
@@ -51,7 +51,7 @@ const MAX_REPORTS = 1500
const { data: rawReportsData } = useQuery({
queryKey: ['reports', MAX_REPORTS],
- queryFn: () => client.labrinth.reports_v3.list({ count: MAX_REPORTS }),
+ queryFn: () => client.labrinth.reports_v3.list({ count: MAX_REPORTS, all: false }),
placeholderData: [],
})
@@ -67,6 +67,13 @@ const versionReports = computed(() =>
const versionIds = computed(() => [
...new Set(versionReports.value.map((report) => report.item_id)),
])
+const sharedInstanceIds = computed(() => [
+ ...new Set(
+ rawReports.value
+ .filter((report) => report.item_type === 'shared-instance')
+ .map((report) => report.item_id),
+ ),
+])
const userIds = computed(() => [...new Set(reporterUsers.value.concat(reportedUsers.value))])
const threadIds = computed(() => [
...new Set(
@@ -94,6 +101,21 @@ const { data: versions } = useQuery({
placeholderData: [],
})
+const { data: sharedInstances } = useQuery({
+ queryKey: computed(() => ['shared-instances', sharedInstanceIds.value]),
+ queryFn: async () => {
+ const results = await Promise.allSettled(
+ sharedInstanceIds.value.map(async (id) => ({
+ id,
+ instance: await client.sharedinstances.instances_v1.get(id),
+ })),
+ )
+ return results.flatMap((result) => (result.status === 'fulfilled' ? [result.value] : []))
+ },
+ enabled: computed(() => sharedInstanceIds.value.length > 0),
+ placeholderData: [],
+})
+
const { data: threads } = useQuery({
queryKey: computed(() => ['threads', threadIds.value]),
queryFn: () =>
@@ -123,6 +145,9 @@ const { data: projects } = useQuery({
const userMap = computed(() => new Map(users.value.map((u) => [u.id, u])))
const versionMap = computed(() => new Map(versions.value.map((v) => [v.id, v])))
const projectMap = computed(() => new Map(projects.value.map((p) => [p.id, p])))
+const sharedInstanceMap = computed(
+ () => new Map(sharedInstances.value.map(({ id, instance }) => [id, instance])),
+)
const threadMap = computed(() => new Map(threads.value.map((t) => [t.id, t])))
const reports = computed(() =>
@@ -136,6 +161,8 @@ const reports = computed(() =>
} else if (report.item_type === 'version') {
enrichedReport.version = versionMap.value.get(report.item_id)
enrichedReport.project = projectMap.value.get(enrichedReport.version?.project_id)
+ } else if (report.item_type === 'shared-instance') {
+ enrichedReport.shared_instance = sharedInstanceMap.value.get(report.item_id)
}
if (report.thread_id) {
const thread = threadMap.value.get(report.thread_id)
@@ -149,7 +176,7 @@ const reports = computed(() =>
const filteredReports = computed(() =>
reports.value?.filter(
(x) =>
- (props.moderation || x.reporterUser?.id === props.auth.user.id) &&
+ (props.moderation || x.reporter === props.auth.user.id) &&
(viewMode.value === 'open' ? x.open : !x.open) &&
(reasonFilter.value === 'All' || reasonFilter.value === x.report_type),
),
diff --git a/apps/frontend/src/pages/dashboard/reports.vue b/apps/frontend/src/pages/dashboard/reports.vue
index e538b5e480..960f7c7cb1 100644
--- a/apps/frontend/src/pages/dashboard/reports.vue
+++ b/apps/frontend/src/pages/dashboard/reports.vue
@@ -1,9 +1,9 @@
-
-
- {{ formatMessage(messages.reportsTitle) }}
-
-
+
+
+ {{ formatMessage(messages.reportsTitle) }}
+
+
diff --git a/packages/ui/src/components/base/StyledInput.vue b/packages/ui/src/components/base/StyledInput.vue
index ad0573b57a..31a9ca57d5 100644
--- a/packages/ui/src/components/base/StyledInput.vue
+++ b/packages/ui/src/components/base/StyledInput.vue
@@ -142,6 +142,7 @@ const props = withDefaults(
min?: number
max?: number
step?: number
+ clamp?: boolean
disabled?: boolean
readonly?: boolean
error?: boolean
@@ -159,6 +160,7 @@ const props = withDefaults(
type: 'text',
size: 'standard',
variant: 'filled',
+ clamp: false,
disabled: false,
readonly: false,
error: false,
@@ -189,12 +191,22 @@ defineExpose({
function onInput(event: Event) {
const target = event.target as HTMLInputElement | HTMLTextAreaElement
- model.value =
- props.type === 'number' && !props.multiline
- ? target.value === ''
- ? undefined
- : Number(target.value)
- : target.value
+ if (props.type !== 'number' || props.multiline) {
+ model.value = target.value
+ return
+ }
+ if (target.value === '') {
+ model.value = undefined
+ return
+ }
+
+ let value = Number(target.value)
+ if (props.clamp) {
+ if (props.min !== undefined) value = Math.max(props.min, value)
+ if (props.max !== undefined) value = Math.min(props.max, value)
+ target.value = String(value)
+ }
+ model.value = value
}
function clear() {
diff --git a/packages/ui/src/components/sharing/invite-players-modal/index.vue b/packages/ui/src/components/sharing/invite-players-modal/index.vue
index 038c7e8e8f..0499ee1af7 100644
--- a/packages/ui/src/components/sharing/invite-players-modal/index.vue
+++ b/packages/ui/src/components/sharing/invite-players-modal/index.vue
@@ -135,6 +135,7 @@
ref="inviteLinkEditor"
:link-expires-at="linkExpiresAt"
:link-max-uses="linkMaxUses"
+ :link-max-uses-limit="linkMaxUsesLimit"
:update-invite-link="updateInviteLink"
/>
@@ -169,6 +170,7 @@ const props = withDefaults(
link?: string
linkExpiresAt?: string | Date | null
linkMaxUses?: number
+ linkMaxUsesLimit?: number
updateInviteLink?: (settings: InviteLinkSettings) => Promise
friendsLabel?: string
searchPlaceholder?: string
@@ -188,6 +190,7 @@ const props = withDefaults(
suggestions: () => [],
canInvite: true,
linkMaxUses: 10,
+ linkMaxUsesLimit: 2147483647,
},
)
diff --git a/packages/ui/src/components/sharing/invite-players-modal/invite-players-modal-invite-link-editor.vue b/packages/ui/src/components/sharing/invite-players-modal/invite-players-modal-invite-link-editor.vue
index 60264bc2d1..433b42085e 100644
--- a/packages/ui/src/components/sharing/invite-players-modal/invite-players-modal-invite-link-editor.vue
+++ b/packages/ui/src/components/sharing/invite-players-modal/invite-players-modal-invite-link-editor.vue
@@ -1,19 +1,62 @@
-
+
{{ formatMessage(messages.expiryLabel) }}
-
+ :dropdown-min-width="customExpiryOpen ? '20rem' : undefined"
+ :dropdown-class="customExpiryOpen ? 'bg-transparent border-0 -mt-1 pb-2 shadow-none' : ''"
+ @open="handleExpiryPickerOpen"
+ @close="handleExpiryPickerClose"
+ @select="selectExpiryPreset"
+ >
+
+
+
+
+
+
+ {{ formatMessage(messages.cancel) }}
+
+
+
+
+ {{ formatMessage(messages.apply) }}
+
+
+
+
+
+ {{ formatMessage(messages.customExpiry) }}
+
+
+
{{ formatMessage(messages.maxUsesLabel) }}
@@ -21,9 +64,10 @@
v-model="maxUses"
type="number"
:min="1"
- :max="2147483647"
+ :max="maximumUses"
:step="1"
- :disabled="saving"
+ :disabled="saving || maximumUses === 0"
+ clamp
/>
@@ -49,29 +93,58 @@
diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
index 831866e867..930615c2f5 100644
--- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
+++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue
@@ -29,6 +29,7 @@ import type {
ContentCardProject,
ContentCardVersion,
ContentOwner,
+ ContentSource,
} from '../types'
const { formatMessage } = useVIntl()
@@ -46,6 +47,7 @@ interface Props {
version?: ContentCardVersion
versionLink?: string | RouteLocationRaw
owner?: ContentOwner
+ source?: ContentSource
enabled?: boolean
installing?: boolean
hasUpdate?: boolean
@@ -68,6 +70,7 @@ const props = withDefaults(defineProps(), {
version: undefined,
versionLink: undefined,
owner: undefined,
+ source: undefined,
enabled: undefined,
installing: false,
hasUpdate: false,
@@ -196,8 +199,32 @@ const deleteHovered = ref(false)
+
+
+
+
+ {{ source.project.title }}
+
+
+
(() =>
: `https://modrinth.com/organization/${item.owner.id}`,
}
: undefined,
+ source: item.source
+ ? {
+ ...item.source,
+ link: item.source.link ?? sourceProjectLink(item.source.project),
+ }
+ : undefined,
...(props.enableToggle ? { enabled: item.enabled } : {}),
installing: item.installing === true,
toggleDisabled: props.actionDisabled,
@@ -293,7 +301,7 @@ const tableItems = computed(() =>
})),
)
const externalItemIds = computed(
- () => new Set(items.value.filter((item) => item.external).map((item) => item.id)),
+ () => new Set(items.value.filter((item) => item.external && !item.source).map((item) => item.id)),
)
const externalSlicerUrls = computed(() => {
const urls: Record = {}
@@ -335,6 +343,12 @@ function itemDisplayName(item: ContentItem) {
return item.project?.title ?? item.file_name
}
+function sourceProjectLink(project: ContentCardProject) {
+ const projectId = project.slug ?? project.id
+ const url = `https://modrinth.com/modpack/${encodeURIComponent(projectId)}`
+ return pageContext ? () => pageContext.openExternalUrl(url) : url
+}
+
function handleEnabledChange(id: string, value: boolean) {
if (props.actionDisabled) return
const item = items.value.find((item) => item.id === id)
diff --git a/packages/ui/src/layouts/shared/content-tab/types.ts b/packages/ui/src/layouts/shared/content-tab/types.ts
index c92f8216a9..b643787f58 100644
--- a/packages/ui/src/layouts/shared/content-tab/types.ts
+++ b/packages/ui/src/layouts/shared/content-tab/types.ts
@@ -21,6 +21,11 @@ export interface ContentOwner {
link?: string | RouteLocationRaw | (() => void)
}
+export interface ContentSource {
+ project: ContentCardProject
+ link?: string | RouteLocationRaw | (() => void)
+}
+
export type ClientWarningType = 'retained' | 'depends' | 'environment'
export type ContentSourceKind =
@@ -44,6 +49,7 @@ export interface ContentCardTableItem {
version?: ContentCardVersion
versionLink?: string | RouteLocationRaw
owner?: ContentOwner
+ source?: ContentSource
enabled?: boolean
disabled?: boolean
disabledTooltip?: string | null
diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json
index f413e28eaf..87e9e9058f 100644
--- a/packages/ui/src/locales/en-US/index.json
+++ b/packages/ui/src/locales/en-US/index.json
@@ -5240,6 +5240,9 @@
"sharing.invite-players-modal.already-invited": {
"defaultMessage": "This user has already been invited."
},
+ "sharing.invite-players-modal.apply-button": {
+ "defaultMessage": "Apply"
+ },
"sharing.invite-players-modal.avatar-alt": {
"defaultMessage": "{username}'s avatar"
},
@@ -5249,12 +5252,36 @@
"sharing.invite-players-modal.cancel-button": {
"defaultMessage": "Cancel"
},
+ "sharing.invite-players-modal.custom-expiry": {
+ "defaultMessage": "Custom..."
+ },
+ "sharing.invite-players-modal.custom-expiry-value": {
+ "defaultMessage": "Custom: {date}"
+ },
"sharing.invite-players-modal.edit-invite-link": {
"defaultMessage": "Edit invite link."
},
"sharing.invite-players-modal.edit-invite-link-title": {
"defaultMessage": "Edit invite link"
},
+ "sharing.invite-players-modal.expiry-in-one-day": {
+ "defaultMessage": "In 1 day"
+ },
+ "sharing.invite-players-modal.expiry-in-one-hour": {
+ "defaultMessage": "In 1 hour"
+ },
+ "sharing.invite-players-modal.expiry-in-seven-days": {
+ "defaultMessage": "In 7 days"
+ },
+ "sharing.invite-players-modal.expiry-in-six-hours": {
+ "defaultMessage": "In 6 hours"
+ },
+ "sharing.invite-players-modal.expiry-in-three-days": {
+ "defaultMessage": "In 3 days"
+ },
+ "sharing.invite-players-modal.expiry-in-twelve-hours": {
+ "defaultMessage": "In 12 hours"
+ },
"sharing.invite-players-modal.expiry-label": {
"defaultMessage": "Expiry date"
},
From fe69e04785034dac8f374d975d3cd3fd48076dc0 Mon Sep 17 00:00:00 2001
From: ThatGravyBoat
Date: Tue, 28 Jul 2026 17:08:56 -0230
Subject: [PATCH 004/145] feat: moderation settings (#6895)
* feat: moderation settings
* chore: move moderation specific feature flag to settings
* feat: force open collapsed regions if opened in new tab
* chore: remove unused field
* chore: run prepr
---
.../ui/moderation/ModerationReportCard.vue | 3 +-
.../ui/moderation/ModerationTechRevCard.vue | 3 +-
.../moderation/checklist/ChecklistKeybind.vue | 116 -------------
.../checklist/ChecklistKeybindsModal.vue | 71 --------
.../checklist/ModerationChecklist.vue | 147 ++++++----------
.../moderation/settings/ModerationKeybind.vue | 164 ++++++++++++++++++
.../settings/ModerationKeybinds.vue | 27 +++
.../settings/ModerationSettings.vue | 87 ++++++++++
.../components/ui/thread/ThreadMessage.vue | 5 +-
apps/frontend/src/composables/featureFlags.ts | 1 -
apps/frontend/src/composables/moderation.ts | 94 ++++++----
apps/frontend/src/pages/[type]/[project].vue | 57 +++---
.../src/pages/moderation/reports/[report].vue | 2 +-
.../src/pages/moderation/reports/index.vue | 7 +-
.../moderation/technical-review/[project].vue | 1 +
.../moderation/technical-review/index.vue | 1 +
apps/frontend/src/pages/settings.vue | 10 ++
.../src/pages/settings/moderation/index.vue | 13 ++
packages/moderation/src/data/keybinds.ts | 51 +++++-
packages/moderation/src/data/settings.ts | 33 ++++
packages/moderation/src/handles/keybinds.ts | 82 +++++++++
packages/moderation/src/handles/settings.ts | 25 +++
packages/moderation/src/index.ts | 4 +
packages/moderation/src/types/keybinds.ts | 81 +++------
packages/moderation/src/types/settings.ts | 29 ++++
25 files changed, 698 insertions(+), 416 deletions(-)
delete mode 100644 apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybind.vue
delete mode 100644 apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybindsModal.vue
create mode 100644 apps/frontend/src/components/ui/moderation/settings/ModerationKeybind.vue
create mode 100644 apps/frontend/src/components/ui/moderation/settings/ModerationKeybinds.vue
create mode 100644 apps/frontend/src/components/ui/moderation/settings/ModerationSettings.vue
create mode 100644 apps/frontend/src/pages/settings/moderation/index.vue
create mode 100644 packages/moderation/src/data/settings.ts
create mode 100644 packages/moderation/src/handles/keybinds.ts
create mode 100644 packages/moderation/src/handles/settings.ts
create mode 100644 packages/moderation/src/types/settings.ts
diff --git a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue
index 6e2a91f743..3566044b4b 100644
--- a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue
+++ b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue
@@ -300,6 +300,7 @@ type SharedInstanceVersionDependency = Labrinth.Versions.v2.Dependency & {
const props = defineProps<{
report: ExtendedReport
+ collapsed: boolean
sharedInstanceDetailsLoader?: () => Promise
sharedInstanceVersionContentLoader?: (
instanceId: string,
@@ -311,7 +312,7 @@ const reportThread = ref<{
setReplyContent: (content: string) => void
sendReply: (privateMessage?: boolean) => Promise
} | null>(null)
-const isThreadCollapsed = ref(true)
+const isThreadCollapsed = ref(props.collapsed)
const sharedInstanceDetails = ref(null)
const sharedInstanceLoading = ref(false)
const sharedInstanceError = ref(null)
diff --git a/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue b/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue
index f91bd67552..d328b054ed 100644
--- a/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue
+++ b/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue
@@ -94,6 +94,7 @@ const props = defineProps<{
focusedDetailId?: string | null
loadingIssues: Set
decompiledSources: Map
+ collapsed: boolean
}>()
const { addNotification } = injectNotificationManager()
@@ -173,7 +174,7 @@ type Tab = 'Thread' | 'Files' | 'File'
const tabs: readonly ('Thread' | 'Files')[] = ['Thread', 'Files']
const currentTab = ref('Thread')
-const isThreadCollapsed = ref(true)
+const isThreadCollapsed = ref(props.collapsed)
const remainingMessageCount = computed(() => {
if (!props.item.thread?.messages) return 0
diff --git a/apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybind.vue b/apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybind.vue
deleted file mode 100644
index 5ad60c0fa4..0000000000
--- a/apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybind.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-
-
-
- {{ toDisplay(definition) }}
-
-
-
-
-
-
-
diff --git a/apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybindsModal.vue b/apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybindsModal.vue
deleted file mode 100644
index 5c0da57c65..0000000000
--- a/apps/frontend/src/components/ui/moderation/checklist/ChecklistKeybindsModal.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
-
-
- {{ keybind.description }}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue
index 21acd31a13..3727b6197c 100644
--- a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue
+++ b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue
@@ -1,5 +1,4 @@
-
@@ -52,11 +56,6 @@
{{ checklistTitleText }}
-
-
-
-
-
@@ -370,7 +369,6 @@ import {
CheckIcon,
DropdownIcon,
FileTextIcon,
- KeyboardIcon,
LeftArrowIcon,
LinkIcon,
ListBulletedIcon,
@@ -383,12 +381,13 @@ import {
UndoIcon,
XIcon,
} from '@modrinth/assets'
-import type {
- IdentifiedNodeBuilder,
- NodeState,
- Priority,
- StageNodeBuilder,
- ValueNodeBuilder,
+import {
+ type IdentifiedNodeBuilder,
+ moderationSettings,
+ type NodeState,
+ type Priority,
+ type StageNodeBuilder,
+ type ValueNodeBuilder,
} from '@modrinth/moderation'
import {
createTrackedPatch,
@@ -396,7 +395,6 @@ import {
expandVariables,
getBooleanChildState,
GLOBAL_STATE_KEY,
- handleKeybind,
isNodeActive,
kebabToTitleCase,
NodeBuilder,
@@ -438,15 +436,14 @@ import type { LockAcquireResponse } from '~/services/moderation-queue.ts'
import { useModerationQueue } from '~/services/moderation-queue.ts'
import { type ActiveAction, type LiveNode, NODE_META_KEY, STATE_KEY } from './checklist-context'
-import KeybindsModal from './ChecklistKeybindsModal.vue'
import NodeRenderer from './NodeRenderer.vue'
const notifications = injectNotificationManager()
const { addNotification } = notifications
const debug = useDebugLogger('ModerationChecklist')
const keybinds = useModerationKeybinds()
+const settings = useModerationSettings()
-const keybindsModal = ref>()
const takeOverModal = ref>()
const props = defineProps<{
@@ -1223,83 +1220,43 @@ interface MessagePart {
content: string
}
-function ignoreLegacyActionKeybind() {
- return undefined
-}
-
function handleKeybinds(event: KeyboardEvent) {
- handleKeybind(
- event,
- {
- project: projectV2.value,
- state: {
- currentStage: currentStage.value,
- totalStages: resolvedStages.value.length,
- currentStageId: currentStageObj.value.id,
- currentStageTitle: currentStageObj.value.label,
+ keybinds.value.handle(event, {
+ project: projectV2.value,
+ scope: 'checklist',
+ state: {
+ currentStage: currentStage.value,
+ totalStages: resolvedStages.value.length,
+ currentStageId: currentStageObj.value.id,
+ currentStageTitle: currentStageObj.value.label,
- isCollapsed: props.collapsed,
- isDone: done.value,
- hasGeneratedMessage: generatedMessage.value,
- isLoadingMessage: loadingMessage.value,
+ isCollapsed: props.collapsed,
+ isDone: done.value,
+ hasGeneratedMessage: generatedMessage.value,
+ isLoadingMessage: loadingMessage.value,
- futureProjectCount: moderationQueue.queueLength,
- visibleActionsCount: resolveChildren(
- currentStageObj.value,
- nodeStates.value[currentStageObj.value.id!] ?? {},
- ).filter((c) => c instanceof NodeBuilder).length,
-
- focusedActionIndex: null,
- focusedActionType: null,
- },
- actions: {
- tryGoNext: nextStage,
- tryGoBack: previousStage,
- tryGenerateMessage: generateMessage,
- trySkipProject: skipCurrentProject,
-
- tryToggleCollapse: () => emit('toggleCollapsed'),
- tryResetProgress: resetProgress,
- tryExitModeration: handleExit,
-
- tryApprove: () => sendMessage(approveSendStatus.value),
- tryReject: () => sendMessage('rejected'),
- tryWithhold: () => sendMessage('withheld'),
- tryEditMessage: previousStage,
-
- tryCopyLink: async (permalink: boolean, relative: boolean, page: boolean) => {
- let url = ``
- if (relative) {
- url += `${globalThis.location.origin}`
- } else {
- url += `https://modrinth.com`
- }
-
- if (permalink) {
- url += `/project/${projectV2.value.id}`
- } else {
- url += `/${projectV2.value.project_type}/${projectV2.value.slug}`
- }
-
- if (page) {
- url += `/${globalThis.location.pathname.split('/').slice(3).join('/')}`
- }
-
- await navigator.clipboard.writeText(url)
- },
-
- tryCopyId: async () => await navigator.clipboard.writeText(projectV2.value.id),
-
- tryToggleAction: ignoreLegacyActionKeybind,
- trySelectDropdownOption: ignoreLegacyActionKeybind,
- tryToggleChip: ignoreLegacyActionKeybind,
- tryFocusNextAction: ignoreLegacyActionKeybind,
- tryFocusPreviousAction: ignoreLegacyActionKeybind,
- tryActivateFocusedAction: ignoreLegacyActionKeybind,
- },
+ futureProjectCount: moderationQueue.queueLength,
+ visibleActionsCount: resolveChildren(
+ currentStageObj.value,
+ nodeStates.value[currentStageObj.value.id!] ?? {},
+ ).filter((c) => c instanceof NodeBuilder).length,
},
- Object.values(keybinds.value),
- )
+ actions: {
+ tryGoNext: nextStage,
+ tryGoBack: previousStage,
+ tryGenerateMessage: generateMessage,
+ trySkipProject: skipCurrentProject,
+
+ tryToggleCollapse: () => emit('toggleCollapsed'),
+ tryResetProgress: resetProgress,
+ tryExitModeration: handleExit,
+
+ tryApprove: () => sendMessage(approveSendStatus.value),
+ tryReject: () => sendMessage('rejected'),
+ tryWithhold: () => sendMessage('withheld'),
+ tryEditMessage: previousStage,
+ },
+ })
}
// Trigger debounced prefetch when user progresses through stages
@@ -1315,7 +1272,9 @@ onMounted(async () => {
window.addEventListener('keydown', handleKeybinds)
window.addEventListener('beforeunload', handleBeforeUnload)
document.addEventListener('visibilitychange', handleVisibilityChange)
- notifications.setNotificationLocation('left')
+ if (settings.value.get(moderationSettings.General.ChecklistPosition) === 'right') {
+ notifications.setNotificationLocation('left')
+ }
const finishedId = localStorage.getItem('moderation-checklist-finished')
if (finishedId === projectV2.value.id) {
@@ -1870,6 +1829,12 @@ const stageOptions = computed(() => {
diff --git a/apps/frontend/src/components/ui/moderation/settings/ModerationKeybinds.vue b/apps/frontend/src/components/ui/moderation/settings/ModerationKeybinds.vue
new file mode 100644
index 0000000000..35ae3a472c
--- /dev/null
+++ b/apps/frontend/src/components/ui/moderation/settings/ModerationKeybinds.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/apps/frontend/src/components/ui/moderation/settings/ModerationSettings.vue b/apps/frontend/src/components/ui/moderation/settings/ModerationSettings.vue
new file mode 100644
index 0000000000..93ea0e6f34
--- /dev/null
+++ b/apps/frontend/src/components/ui/moderation/settings/ModerationSettings.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
{{ name }}
+
+
+
+ {{ setting.title }}
+ {{ setting.description }}
+
+ Default:
+ {{ setting.default }}
+
+
+
+ configuredSettings.set(setting, value)"
+ />
+ configuredSettings.set(setting, value)"
+ />
+ configuredSettings.set(setting, (event.target as HTMLInputElement).value)
+ "
+ />
+
+
+
+
diff --git a/apps/frontend/src/components/ui/thread/ThreadMessage.vue b/apps/frontend/src/components/ui/thread/ThreadMessage.vue
index 1d615d9ee2..303e197633 100644
--- a/apps/frontend/src/components/ui/thread/ThreadMessage.vue
+++ b/apps/frontend/src/components/ui/thread/ThreadMessage.vue
@@ -5,7 +5,7 @@
'has-body': message.body.type === 'text' && !forceCompact,
'no-actions': noLinks,
private: isPrivateMessage,
- 'show-private-bg': flags.showModeratorPrivateMessageHighlight,
+ 'show-private-bg': settings.get(moderationSettings.General.PrivateMessageHighlight),
}"
>
@@ -149,6 +149,7 @@ import {
ScaleIcon,
TrashIcon,
} from '@modrinth/assets'
+import { moderationSettings } from '@modrinth/moderation'
import {
AutoLink,
Avatar,
@@ -194,7 +195,7 @@ const props = defineProps({
})
const emit = defineEmits(['update-thread'])
-const flags = useFeatureFlags()
+const settings = useModerationSettings()
const formattedMessage = computed(() => {
const body = renderString(props.message.body.body)
diff --git a/apps/frontend/src/composables/featureFlags.ts b/apps/frontend/src/composables/featureFlags.ts
index 956da2e300..3dcc90f86e 100644
--- a/apps/frontend/src/composables/featureFlags.ts
+++ b/apps/frontend/src/composables/featureFlags.ts
@@ -53,7 +53,6 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
alwaysIgnoreErrorBanner: false,
showViewProdRouteBanner: false,
showModeratorProjectMemberUi: false,
- showModeratorPrivateMessageHighlight: true,
archonApiStaging: false,
showHostingAccessInstanceAuditLog: false,
versionDevInfoCollapsed: true,
diff --git a/apps/frontend/src/composables/moderation.ts b/apps/frontend/src/composables/moderation.ts
index 6429984894..ca4f06d19a 100644
--- a/apps/frontend/src/composables/moderation.ts
+++ b/apps/frontend/src/composables/moderation.ts
@@ -1,61 +1,81 @@
-import {
- type KeybindDefinition,
- type KeybindListener,
- keybinds,
- normalizeKeybind,
-} from '@modrinth/moderation'
+import { type KeybindDefinition, Keybinds, Settings } from '@modrinth/moderation'
+import { computed } from 'vue'
import type { CookieOptions } from '#app'
const moderationKeybindsId = 'moderation-keybinds'
+const moderationSettingsId = 'moderation-settings'
type StoredKeybinds = { [id: string]: KeybindDefinition[] }
-type PartialStoredKeybinds = Partial
+type StoredSettings = { [id: string]: any }
+type StoredOptions = {
+ keybinds: Partial
+ settings: Partial
+}
-const getCookieOptions = () =>
+const getCookieOptions = () =>
({
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: useRuntimeConfig().public.cookieSecure,
httpOnly: false,
path: '/',
- }) satisfies CookieOptions
+ }) satisfies CookieOptions
-export const useModerationKeybinds = () =>
- useState<{ [id: string]: KeybindListener }>(moderationKeybindsId, () => {
- const storedKeybinds = useCookie(
- moderationKeybindsId,
- getCookieOptions(),
- )
+const useModerationCookies = () => {
+ const keybindCookie = useCookie | null>(
+ moderationKeybindsId,
+ getCookieOptions(),
+ )
+ const optionsCookie = useCookie>(moderationSettingsId, getCookieOptions())
- if (!storedKeybinds.value) {
- storedKeybinds.value = {}
+ if (keybindCookie.value && !optionsCookie.value) {
+ optionsCookie.value = {
+ keybinds: keybindCookie.value,
+ settings: {},
+ }
+ keybindCookie.value = null
+ } else if (keybindCookie.value && optionsCookie.value) {
+ keybindCookie.value = null // options is the new cookie so it will override the existing keybinds.
+ }
+
+ return optionsCookie
+}
+
+const useModerationOptions = () =>
+ useState<{ keybinds: StoredKeybinds; settings: StoredSettings }>(moderationKeybindsId, () => {
+ const cookie = useModerationCookies()
+
+ const keybindOutput: StoredKeybinds = {}
+
+ for (const [id, definition] of Object.entries(cookie.value.keybinds || {})) {
+ if (!definition) continue
+ keybindOutput[id] = definition
}
- const output: { [id: string]: KeybindListener } = {}
-
- for (const [id, keybind] of Object.entries(keybinds)) {
- const definitions = storedKeybinds.value[id]
- output[id] = {
- keybind: definitions !== undefined ? definitions : keybind.keybind,
- description: keybind.description,
- enabled: keybind.enabled,
- action: keybind.action,
- }
+ const settingsOutput: StoredSettings = {}
+ for (const [id, setting] of Object.entries(cookie.value.settings || {})) {
+ settingsOutput[id] = setting
}
- return output
+ return {
+ keybinds: keybindOutput,
+ settings: settingsOutput,
+ }
})
-export const saveModerationKeybinds = () => {
- const keybinds = useModerationKeybinds()
- const cookie = useCookie(moderationKeybindsId, getCookieOptions())
+export const useModerationKeybinds = () =>
+ computed(() => new Keybinds(useModerationOptions().value.keybinds))
- const storedKeybinds: PartialStoredKeybinds = {}
- for (const [id, keybind] of Object.entries(keybinds.value)) {
- storedKeybinds[id] = (Array.isArray(keybind.keybind) ? keybind.keybind : [keybind.keybind]).map(
- normalizeKeybind,
- )
+export const useModerationSettings = () =>
+ computed(() => new Settings(useModerationOptions().value.settings, saveModerationOptions))
+
+export const saveModerationOptions = () => {
+ const options = useModerationOptions()
+ const cookie = useModerationCookies()
+
+ cookie.value = {
+ keybinds: options.value.keybinds,
+ settings: options.value.settings,
}
- cookie.value = storedKeybinds
}
diff --git a/apps/frontend/src/pages/[type]/[project].vue b/apps/frontend/src/pages/[type]/[project].vue
index b413ebb274..456d8b2a82 100644
--- a/apps/frontend/src/pages/[type]/[project].vue
+++ b/apps/frontend/src/pages/[type]/[project].vue
@@ -469,16 +469,12 @@
-
-
-
+ :collapsed="collapsedModerationChecklist"
+ @exit="setModerationChecklistOpen(false)"
+ @toggle-collapsed="collapsedModerationChecklist = !collapsedModerationChecklist"
+ />
@@ -505,6 +501,7 @@ import {
SettingsIcon,
XIcon,
} from '@modrinth/assets'
+import { moderationSettings } from '@modrinth/moderation'
import {
Admonition,
Avatar,
@@ -535,7 +532,7 @@ import {
useStickyObserver,
useVIntl,
} from '@modrinth/ui'
-import { formatProjectType } from '@modrinth/utils'
+import { formatProjectType, isStaff } from '@modrinth/utils'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { useLocalStorage } from '@vueuse/core'
import { Tooltip } from 'floating-vue'
@@ -572,6 +569,8 @@ const router = useRouter()
const signInRouteObj = computed(() => getSignInRouteObj(route))
const config = useRuntimeConfig()
const moderationQueue = useModerationQueue()
+const keybinds = useModerationKeybinds()
+const modSettings = useModerationSettings()
const notifications = injectNotificationManager()
const { addNotification } = notifications
@@ -1965,6 +1964,25 @@ async function deleteVersion(id) {
stopLoading()
}
+// moderation project keybinds
+
+onMounted(() => window.addEventListener('keydown', handleKeybinds))
+onUnmounted(() => window.removeEventListener('keydown', handleKeybinds))
+
+function handleKeybinds(event) {
+ if (!isStaff(auth.value.user)) return
+ if (
+ !showModerationChecklist.value &&
+ !modSettings.value.get(moderationSettings.General.ProjectKeybinds)
+ )
+ return
+
+ keybinds.value.handle(event, {
+ project: projectRaw.value,
+ scope: 'project',
+ })
+}
+
const navLinks = computed(() => {
const routeType = route.params.type || project.value.project_type
const projectUrl = `/${routeType}/${project.value.slug ? project.value.slug : project.value.id}`
@@ -2189,26 +2207,7 @@ provideProjectPageContext({
}
}
-.moderation-checklist {
- position: fixed;
- bottom: 1rem;
- right: 1rem;
- overflow-y: auto;
- z-index: 50;
- transition: bottom 0.25s ease-in-out;
-
- > div {
- box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
- }
-}
-
.new-page {
column-gap: 1.5rem;
}
-
-
diff --git a/apps/frontend/src/pages/moderation/reports/[report].vue b/apps/frontend/src/pages/moderation/reports/[report].vue
index 806545561c..fdd656cafe 100644
--- a/apps/frontend/src/pages/moderation/reports/[report].vue
+++ b/apps/frontend/src/pages/moderation/reports/[report].vue
@@ -28,6 +28,6 @@ const { data: report } = useQuery({
-
+
diff --git a/apps/frontend/src/pages/moderation/reports/index.vue b/apps/frontend/src/pages/moderation/reports/index.vue
index e2728ef956..69b82307dc 100644
--- a/apps/frontend/src/pages/moderation/reports/index.vue
+++ b/apps/frontend/src/pages/moderation/reports/index.vue
@@ -177,7 +177,12 @@
diff --git a/apps/frontend/src/pages/moderation/technical-review/[project].vue b/apps/frontend/src/pages/moderation/technical-review/[project].vue
index e4da9ba339..af938bca01 100644
--- a/apps/frontend/src/pages/moderation/technical-review/[project].vue
+++ b/apps/frontend/src/pages/moderation/technical-review/[project].vue
@@ -305,6 +305,7 @@ function refetch() {
:focused-detail-id="focusedDetailId"
:loading-issues="loadingIssues"
:decompiled-sources="decompiledSources"
+ :collapsed="false"
@refetch="refetch"
@load-issue-sources="handleLoadIssueSources"
@mark-complete="handleMarkComplete"
diff --git a/apps/frontend/src/pages/moderation/technical-review/index.vue b/apps/frontend/src/pages/moderation/technical-review/index.vue
index 9c9773c2a9..d66a0a4b2f 100644
--- a/apps/frontend/src/pages/moderation/technical-review/index.vue
+++ b/apps/frontend/src/pages/moderation/technical-review/index.vue
@@ -719,6 +719,7 @@ watch(totalPages, (pages) => {
:item="item"
:loading-issues="loadingIssues"
:decompiled-sources="decompiledSources"
+ :collapsed="true"
@refetch="refetch"
@load-issue-sources="handleLoadIssueSources"
@mark-complete="handleMarkComplete"
diff --git a/apps/frontend/src/pages/settings.vue b/apps/frontend/src/pages/settings.vue
index f06cb13dfc..f4381fec76 100644
--- a/apps/frontend/src/pages/settings.vue
+++ b/apps/frontend/src/pages/settings.vue
@@ -79,6 +79,14 @@
icon: ServerIcon,
}
: null,
+ isStaff(auth.user) ? { type: 'heading', label: 'Staff' } : null,
+ isStaff(auth.user)
+ ? {
+ link: '/settings/moderation',
+ label: 'Moderation',
+ icon: ScaleIcon,
+ }
+ : null,
].filter(Boolean)
"
/>
@@ -95,6 +103,7 @@ import {
LanguagesIcon,
MonitorSmartphoneIcon,
PaintbrushIcon,
+ ScaleIcon,
ServerIcon,
ShieldIcon,
ToggleRightIcon,
@@ -107,6 +116,7 @@ import {
NormalPage,
useVIntl,
} from '@modrinth/ui'
+import { isStaff } from '@modrinth/utils'
import NavStack from '~/components/ui/NavStack.vue'
diff --git a/apps/frontend/src/pages/settings/moderation/index.vue b/apps/frontend/src/pages/settings/moderation/index.vue
new file mode 100644
index 0000000000..71df2b42e0
--- /dev/null
+++ b/apps/frontend/src/pages/settings/moderation/index.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/packages/moderation/src/data/keybinds.ts b/packages/moderation/src/data/keybinds.ts
index b86029e2ac..25b2994374 100644
--- a/packages/moderation/src/data/keybinds.ts
+++ b/packages/moderation/src/data/keybinds.ts
@@ -1,77 +1,118 @@
import type { KeybindListener } from '../types/keybinds'
+import type { Labrinth } from '@modrinth/api-client'
+
+const copyProjectLink = async (
+ project: Labrinth.Projects.v2.Project,
+ permalink: boolean,
+ relative: boolean,
+ page: boolean,
+) => {
+ let url = ``
+ if (relative) {
+ url += `${globalThis.location.origin}`
+ } else {
+ url += `https://modrinth.com`
+ }
+
+ if (permalink) {
+ url += `/project/${project.id}`
+ } else {
+ url += `/${project.project_type}/${project.slug}`
+ }
+
+ if (page) {
+ url += `/${globalThis.location.pathname.split('/').slice(3).join('/')}`
+ }
+
+ await navigator.clipboard.writeText(url)
+}
const keybinds: { [id: string]: KeybindListener } = {
'next-stage': {
keybind: 'ArrowRight',
description: 'Go to next stage',
+ scope: 'checklist',
enabled: (ctx) => !ctx.state.isDone,
action: (ctx) => ctx.actions.tryGoNext(),
},
'previous-stage': {
keybind: 'ArrowLeft',
description: 'Go to previous stage',
+ scope: 'checklist',
enabled: (ctx) => !ctx.state.isDone,
action: (ctx) => ctx.actions.tryGoBack(),
},
'generate-message': {
keybind: 'Ctrl+Shift+E',
description: 'Generate moderation message',
+ scope: 'checklist',
action: (ctx) => ctx.actions.tryGenerateMessage(),
},
'toggle-collapse': {
keybind: 'Shift+C',
description: 'Toggle collapse/expand',
+ scope: 'checklist',
action: (ctx) => ctx.actions.tryToggleCollapse(),
},
'reset-progress': {
keybind: 'Ctrl+Shift+R',
description: 'Reset moderation progress',
+ scope: 'checklist',
action: (ctx) => ctx.actions.tryResetProgress(),
},
'skip-project': {
keybind: 'Ctrl+Shift+S',
description: 'Skip to next project',
+ scope: 'checklist',
enabled: (ctx) => ctx.state.futureProjectCount > 0 && !ctx.state.isDone,
action: (ctx) => ctx.actions.trySkipProject(),
},
'copy-permalink': {
keybind: 'Ctrl+Alt+C',
description: 'Copy permalink',
- action: (ctx) => ctx.actions.tryCopyLink(true, false, false),
+ scope: 'project',
+ action: async (ctx) => copyProjectLink(ctx.project, true, false, false),
},
'copy-relative-permalink': {
keybind: 'Ctrl+Alt+R',
description: 'Copy relative permalink',
- action: (ctx) => ctx.actions.tryCopyLink(true, true, false),
+ scope: 'project',
+ action: async (ctx) => copyProjectLink(ctx.project, true, true, false),
},
'copy-page-permalink': {
keybind: 'Shift+Ctrl+Alt+C',
description: 'Copy permalink with page',
- action: (ctx) => ctx.actions.tryCopyLink(true, false, true),
+ scope: 'project',
+ action: async (ctx) => copyProjectLink(ctx.project, true, false, true),
},
'copy-page-relative-permalink': {
keybind: 'Shift+Ctrl+Alt+R',
description: 'Copy relative permalink with page',
- action: (ctx) => ctx.actions.tryCopyLink(true, true, true),
+ scope: 'project',
+ action: async (ctx) => copyProjectLink(ctx.project, true, true, true),
},
'copy-id': {
keybind: 'Ctrl+Alt+D',
description: 'Copy Project ID',
- action: (ctx) => ctx.actions.tryCopyId(),
+ scope: 'project',
+ action: async (ctx) => await navigator.clipboard.writeText(ctx.project.id),
},
'approve-project': {
keybind: 'Shift+Alt+A',
description: 'Approve project',
+ scope: 'checklist',
action: (ctx) => ctx.actions.tryApprove(),
},
'withhold-project': {
keybind: 'Shift+Alt+W',
description: 'Withhold project',
+ scope: 'checklist',
action: (ctx) => ctx.actions.tryWithhold(),
},
'reject-project': {
keybind: 'Shift+Alt+R',
description: 'Reject project',
+ scope: 'checklist',
action: (ctx) => ctx.actions.tryReject(),
},
}
diff --git a/packages/moderation/src/data/settings.ts b/packages/moderation/src/data/settings.ts
new file mode 100644
index 0000000000..25c8ccff4a
--- /dev/null
+++ b/packages/moderation/src/data/settings.ts
@@ -0,0 +1,33 @@
+import type { EnumSettingDefinition, ToggleSettingDefinition } from '../types/settings.ts'
+
+const settings = {
+ General: {
+ ChecklistPosition: {
+ type: 'enum',
+ id: 'checklist-position',
+ title: 'Checklist Position',
+ description: 'Where the checklist should be displayed on the page',
+ entries: [
+ { value: 'left', label: 'Left' },
+ { value: 'right', label: 'Right' },
+ ],
+ default: 'right',
+ } as EnumSettingDefinition,
+ ProjectKeybinds: {
+ type: 'toggle',
+ id: 'project-keybinds',
+ title: 'Enable Project Keybinds',
+ description: 'Weather certain keybinds should work without the checklist visible.',
+ default: false,
+ } as ToggleSettingDefinition,
+ PrivateMessageHighlight: {
+ type: 'toggle',
+ id: 'private-message-highlight',
+ title: 'Highlight Private Messages',
+ description: 'Whether private messages should be highlighted in the chat.',
+ default: true,
+ } as ToggleSettingDefinition,
+ },
+} as const
+
+export default settings
diff --git a/packages/moderation/src/handles/keybinds.ts b/packages/moderation/src/handles/keybinds.ts
new file mode 100644
index 0000000000..12a9fae7f3
--- /dev/null
+++ b/packages/moderation/src/handles/keybinds.ts
@@ -0,0 +1,82 @@
+import {
+ type KeybindDefinition,
+ type KeybindListener,
+ matchesKeybind,
+ type ModerationContext,
+ normalizeKeybind,
+} from '../types/keybinds.ts'
+import keybinds from '../data/keybinds.ts'
+
+function normalizeKeybinds(
+ keybind: KeybindDefinition | KeybindDefinition[] | string | string[],
+): KeybindDefinition[] {
+ return Array.isArray(keybind) ? keybind.map(normalizeKeybind) : [normalizeKeybind(keybind)]
+}
+
+export type KeybindListenerWithDefault = KeybindListener & {
+ keybind: KeybindDefinition[]
+ defaultKeybind: KeybindDefinition[]
+}
+
+export class Keybinds {
+ private readonly configured: { [id: string]: KeybindDefinition[] } = {}
+
+ constructor(keybinds: { [id: string]: KeybindDefinition[] }) {
+ this.configured = keybinds
+ }
+
+ *[Symbol.iterator](): IterableIterator<[string, KeybindListenerWithDefault]> {
+ for (const [id, keybind] of Object.entries(keybinds)) {
+ yield [
+ id,
+ {
+ ...keybind,
+ keybind: this.configured[id] ?? normalizeKeybinds(keybind.keybind),
+ defaultKeybind: normalizeKeybinds(keybind.keybind),
+ },
+ ]
+ }
+ }
+
+ set(id: string, keybind: KeybindDefinition | KeybindDefinition[] | string | string[]): void {
+ this.configured[id] = normalizeKeybinds(keybind)
+ }
+
+ handle(event: KeyboardEvent, ctx: ModerationContext): boolean {
+ if (
+ event.target instanceof HTMLInputElement ||
+ event.target instanceof HTMLTextAreaElement ||
+ (event.target as HTMLElement)?.closest('.cm-editor') ||
+ (event.target as HTMLElement)?.classList?.contains('cm-content') ||
+ (event.target as HTMLElement)?.classList?.contains('cm-line')
+ ) {
+ return false
+ }
+
+ for (const [id, keybind] of Object.entries(keybinds)) {
+ if (ctx.scope !== keybind.scope) {
+ continue
+ }
+
+ if (keybind.enabled && !keybind.enabled(ctx as any)) {
+ continue
+ }
+
+ const definitions = this.configured[id] ?? normalizeKeybinds(keybind.keybind)
+ const matches = definitions.some((def) => matchesKeybind(event, def))
+
+ if (matches) {
+ keybind.action(ctx as any)
+
+ const shouldPrevent = definitions.some((def) => def.preventDefault !== false)
+ if (shouldPrevent) {
+ event.preventDefault()
+ }
+
+ return true
+ }
+ }
+
+ return false
+ }
+}
diff --git a/packages/moderation/src/handles/settings.ts b/packages/moderation/src/handles/settings.ts
new file mode 100644
index 0000000000..6109596d8b
--- /dev/null
+++ b/packages/moderation/src/handles/settings.ts
@@ -0,0 +1,25 @@
+import type { SettingDefinitionBase } from '../types/settings.ts'
+
+export class Settings {
+ private readonly settings: { [id: string]: any }
+ private readonly onChange: () => void
+
+ constructor(
+ settings: { [id: string]: any } | undefined = undefined,
+ onChange: () => void = () => {},
+ ) {
+ this.settings = settings || {}
+ this.onChange = onChange
+ }
+
+ get(definition: SettingDefinitionBase): T {
+ return this.settings[definition.id] ?? definition.default
+ }
+
+ set(definition: SettingDefinitionBase, value?: T): void {
+ const previous = this.settings[definition.id] ?? definition.default
+ this.settings[definition.id] = value
+ definition.onChange?.(previous, value ?? definition.default)
+ this.onChange()
+ }
+}
diff --git a/packages/moderation/src/index.ts b/packages/moderation/src/index.ts
index 23ec61f2a7..95d7944884 100644
--- a/packages/moderation/src/index.ts
+++ b/packages/moderation/src/index.ts
@@ -1,5 +1,6 @@
export { default as checklist, stages, useStages } from './data/checklist'
export { default as keybinds } from './data/keybinds'
+export { default as moderationSettings } from './data/settings'
export { default as nags } from './data/nags'
export * from './data/nags/index'
export { default as attributionQuickReplies } from './data/quick-replies/permissions-quick-replies'
@@ -11,6 +12,7 @@ export {
export * from './locales'
export * from './types/actions'
export * from './types/keybinds'
+export * from './types/settings'
export * from './types/messages'
export * from './types/nags'
export * from './types/node'
@@ -19,3 +21,5 @@ export * from './types/quick-reply'
export * from './types/reports'
export * from './types/stage'
export * from './utils'
+export * from './handles/keybinds'
+export * from './handles/settings'
diff --git a/packages/moderation/src/types/keybinds.ts b/packages/moderation/src/types/keybinds.ts
index 5b31222049..529eaccc86 100644
--- a/packages/moderation/src/types/keybinds.ts
+++ b/packages/moderation/src/types/keybinds.ts
@@ -14,17 +14,6 @@ export interface ModerationActions {
tryReject: () => void
tryWithhold: () => void
tryEditMessage: () => void
-
- tryToggleAction: (actionIndex: number) => void
- trySelectDropdownOption: (actionIndex: number, optionIndex: number) => void
- tryToggleChip: (actionIndex: number, chipIndex: number) => void
-
- tryFocusNextAction: () => void
- tryFocusPreviousAction: () => void
- tryActivateFocusedAction: () => void
-
- tryCopyLink: (permalink: boolean, relative: boolean, page: boolean) => void
- tryCopyId: () => void
}
export interface ModerationState {
@@ -41,17 +30,22 @@ export interface ModerationState {
futureProjectCount: number
visibleActionsCount: number
-
- focusedActionIndex: number | null
- focusedActionType: 'button' | 'toggle' | 'dropdown' | 'multi-select' | null
}
-export interface ModerationContext {
+export type ModerationProjectContext = {
project: Labrinth.Projects.v2.Project
+ scope: 'project'
+}
+
+export type ModerationChecklistContext = {
+ project: Labrinth.Projects.v2.Project
+ scope: 'checklist'
state: ModerationState
actions: ModerationActions
}
+export type ModerationContext = ModerationProjectContext | ModerationChecklistContext
+
export interface KeybindDefinition {
key: string
ctrl?: boolean
@@ -61,13 +55,22 @@ export interface KeybindDefinition {
preventDefault?: boolean
}
-export interface KeybindListener {
+export type BaseKeybindListener = {
keybind: KeybindDefinition | KeybindDefinition[] | string | string[]
description: string
- enabled?: (ctx: ModerationContext) => boolean
- action: (ctx: ModerationContext) => void
+ scope: 'project' | 'checklist'
+ enabled?: (ctx: T) => boolean
+ action: (ctx: T) => void
}
+export type KeybindProjectListener = BaseKeybindListener & {
+ scope: 'project'
+}
+export type KeybindChecklistListener = BaseKeybindListener & {
+ scope: 'checklist'
+}
+export type KeybindListener = KeybindProjectListener | KeybindChecklistListener
+
export function parseKeybind(keybindString: string): KeybindDefinition {
const parts = keybindString.split('+').map((p) => p.trim().toLowerCase())
@@ -106,45 +109,3 @@ export function toKeybindDefinition(event: KeyboardEvent): KeybindDefinition {
preventDefault: true,
}
}
-
-export function handleKeybind(
- event: KeyboardEvent,
- ctx: ModerationContext,
- keybinds: KeybindListener[],
-): boolean {
- if (
- event.target instanceof HTMLInputElement ||
- event.target instanceof HTMLTextAreaElement ||
- (event.target as HTMLElement)?.closest('.cm-editor') ||
- (event.target as HTMLElement)?.classList?.contains('cm-content') ||
- (event.target as HTMLElement)?.classList?.contains('cm-line') ||
- document.getElementById('moderation-checklist-keybinds-modal')
- ) {
- return false
- }
-
- for (const keybind of keybinds) {
- if (keybind.enabled && !keybind.enabled(ctx)) {
- continue
- }
-
- const keybindDefs = Array.isArray(keybind.keybind)
- ? keybind.keybind.map(normalizeKeybind)
- : [normalizeKeybind(keybind.keybind)]
-
- const matches = keybindDefs.some((def) => matchesKeybind(event, def))
-
- if (matches) {
- keybind.action(ctx)
-
- const shouldPrevent = keybindDefs.some((def) => def.preventDefault !== false)
- if (shouldPrevent) {
- event.preventDefault()
- }
-
- return true
- }
- }
-
- return false
-}
diff --git a/packages/moderation/src/types/settings.ts b/packages/moderation/src/types/settings.ts
new file mode 100644
index 0000000000..7cf1f91ee7
--- /dev/null
+++ b/packages/moderation/src/types/settings.ts
@@ -0,0 +1,29 @@
+export interface SettingDefinitionBase {
+ type: string
+ id: string
+ title: string
+ description: string
+ default: T
+ onChange?: (previous: T | undefined, current: T) => void
+}
+
+export interface ToggleSettingDefinition extends SettingDefinitionBase {
+ type: 'toggle'
+}
+
+export interface EnumSettingDefinition extends SettingDefinitionBase {
+ type: 'enum'
+ entries: {
+ label: string
+ value: string
+ }[]
+}
+
+export interface StringSettingDefinition extends SettingDefinitionBase {
+ type: 'string'
+}
+
+export type SettingDefinition =
+ | ToggleSettingDefinition
+ | EnumSettingDefinition
+ | StringSettingDefinition
From cbb31f31c003f42207e15fcbd484642bbe1f0ac7 Mon Sep 17 00:00:00 2001
From: Prospector <6166773+Prospector@users.noreply.github.com>
Date: Tue, 28 Jul 2026 12:42:08 -0700
Subject: [PATCH 005/145] cleaup app layout, add missing translations to it
(#6904)
* cleaup app layout, add missing translations to it
* remove feature flag in backend
* add app-messages
* clean up nav slightly
* allow more than 3 quick instances
* make quick instances resizable
---
apps/app-frontend/src/App.vue | 286 ++++++-----------
.../components/ui/QuickInstanceSwitcher.vue | 300 ++++++++++++++++--
.../src/components/ui/SurveyPopup.vue | 233 ++++++++++++++
.../components/ui/world/RecentWorldsList.vue | 10 +-
.../app-frontend/src/locales/ar-SA/index.json | 3 -
.../app-frontend/src/locales/cs-CZ/index.json | 3 -
.../app-frontend/src/locales/da-DK/index.json | 3 -
.../app-frontend/src/locales/de-CH/index.json | 3 -
.../app-frontend/src/locales/de-DE/index.json | 3 -
.../app-frontend/src/locales/en-US/index.json | 60 +++-
.../src/locales/es-419/index.json | 3 -
.../app-frontend/src/locales/es-ES/index.json | 3 -
.../app-frontend/src/locales/fi-FI/index.json | 3 -
.../src/locales/fil-PH/index.json | 3 -
.../app-frontend/src/locales/fr-FR/index.json | 3 -
.../app-frontend/src/locales/he-IL/index.json | 3 -
.../app-frontend/src/locales/hu-HU/index.json | 3 -
.../app-frontend/src/locales/id-ID/index.json | 3 -
.../app-frontend/src/locales/it-IT/index.json | 3 -
.../app-frontend/src/locales/ja-JP/index.json | 3 -
.../app-frontend/src/locales/ko-KR/index.json | 3 -
.../app-frontend/src/locales/ms-MY/index.json | 3 -
.../app-frontend/src/locales/nl-NL/index.json | 3 -
.../app-frontend/src/locales/no-NO/index.json | 3 -
.../app-frontend/src/locales/pl-PL/index.json | 3 -
.../app-frontend/src/locales/pt-BR/index.json | 3 -
.../app-frontend/src/locales/pt-PT/index.json | 3 -
.../app-frontend/src/locales/ru-RU/index.json | 3 -
.../app-frontend/src/locales/sr-CS/index.json | 3 -
.../app-frontend/src/locales/sv-SE/index.json | 3 -
.../app-frontend/src/locales/th-TH/index.json | 3 -
.../app-frontend/src/locales/tr-TR/index.json | 3 -
.../app-frontend/src/locales/uk-UA/index.json | 3 -
.../app-frontend/src/locales/vi-VN/index.json | 3 -
.../app-frontend/src/locales/zh-CN/index.json | 3 -
.../app-frontend/src/locales/zh-TW/index.json | 3 -
apps/app-frontend/src/pages/Browse.vue | 10 +-
apps/app-frontend/src/pages/Skins.vue | 7 +-
apps/app-frontend/src/pages/Worlds.vue | 4 -
apps/app-frontend/src/pages/index.js | 3 +-
apps/app-frontend/src/routes.js | 8 -
apps/app-frontend/src/store/theme.ts | 1 -
apps/app-frontend/src/utils/app-messages.ts | 8 +
apps/frontend/src/layouts/default.vue | 6 +-
apps/frontend/src/locales/cs-CZ/index.json | 3 -
apps/frontend/src/locales/da-DK/index.json | 3 -
apps/frontend/src/locales/de-CH/index.json | 3 -
apps/frontend/src/locales/de-DE/index.json | 3 -
apps/frontend/src/locales/en-US/index.json | 3 -
apps/frontend/src/locales/es-419/index.json | 3 -
apps/frontend/src/locales/es-ES/index.json | 3 -
apps/frontend/src/locales/fil-PH/index.json | 3 -
apps/frontend/src/locales/fr-FR/index.json | 3 -
apps/frontend/src/locales/he-IL/index.json | 3 -
apps/frontend/src/locales/hu-HU/index.json | 3 -
apps/frontend/src/locales/id-ID/index.json | 3 -
apps/frontend/src/locales/it-IT/index.json | 3 -
apps/frontend/src/locales/ja-JP/index.json | 3 -
apps/frontend/src/locales/ko-KR/index.json | 3 -
apps/frontend/src/locales/ms-MY/index.json | 3 -
apps/frontend/src/locales/nl-NL/index.json | 3 -
apps/frontend/src/locales/no-NO/index.json | 3 -
apps/frontend/src/locales/pl-PL/index.json | 3 -
apps/frontend/src/locales/pt-BR/index.json | 3 -
apps/frontend/src/locales/pt-PT/index.json | 3 -
apps/frontend/src/locales/ro-RO/index.json | 3 -
apps/frontend/src/locales/ru-RU/index.json | 3 -
apps/frontend/src/locales/sv-SE/index.json | 3 -
apps/frontend/src/locales/tr-TR/index.json | 3 -
apps/frontend/src/locales/uk-UA/index.json | 3 -
apps/frontend/src/locales/vi-VN/index.json | 3 -
apps/frontend/src/locales/zh-CN/index.json | 3 -
apps/frontend/src/locales/zh-TW/index.json | 3 -
packages/app-lib/src/state/settings.rs | 1 -
packages/assets/generated-icons.ts | 2 +
packages/assets/icons/shirt.svg | 15 +
.../components/ContentModpackCard.vue | 2 +-
packages/ui/src/locales/en-US/index.json | 3 +
packages/ui/src/utils/common-messages.ts | 4 +
79 files changed, 689 insertions(+), 454 deletions(-)
create mode 100644 apps/app-frontend/src/components/ui/SurveyPopup.vue
delete mode 100644 apps/app-frontend/src/pages/Worlds.vue
create mode 100644 apps/app-frontend/src/utils/app-messages.ts
create mode 100644 packages/assets/icons/shirt.svg
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index f7e78a4963..87263b4af8 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -10,24 +10,20 @@ import {
} from '@modrinth/api-client'
import {
ArrowBigUpDashIcon,
- ChangeSkinIcon,
CompassIcon,
- ExternalIcon,
HomeIcon,
LeftArrowIcon,
LibraryIcon,
LogInIcon,
LogOutIcon,
NewspaperIcon,
- NotepadTextIcon,
PlusIcon,
RefreshCwIcon,
RightArrowIcon,
ServerStackIcon,
SettingsIcon,
+ ShirtIcon,
UserIcon,
- WorldIcon,
- XIcon,
} from '@modrinth/assets'
import {
Admonition,
@@ -39,6 +35,7 @@ import {
CreationFlowModal,
defineMessages,
I18nDebugPanel,
+ IntlFormatted,
LoadingBar,
NewsArticleCard,
NotificationPanel,
@@ -63,7 +60,6 @@ import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
import { openUrl } from '@tauri-apps/plugin-opener'
import { type } from '@tauri-apps/plugin-os'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
-import { $fetch } from 'ofetch'
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
@@ -88,6 +84,7 @@ import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import SharedInstanceInviteHandler from '@/components/ui/shared-instances/shared-instance-invite-handler/index.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
+import SurveyPopup from '@/components/ui/SurveyPopup.vue'
import WindowControls from '@/components/ui/WindowControls.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { config } from '@/config'
@@ -104,12 +101,7 @@ import { check_reachable } from '@/helpers/auth.js'
import { get_user, get_version } from '@/helpers/cache.js'
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install'
-import {
- can_current_user_use_shared_instances,
- get as getInstance,
- list,
- run,
-} from '@/helpers/instance'
+import { can_current_user_use_shared_instances, get as getInstance, run } from '@/helpers/instance'
import { get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
@@ -148,6 +140,7 @@ import { setupAuthProvider } from '@/providers/setup/auth'
import { setupLoadingStateProvider } from '@/providers/setup/loading-state'
import { useError } from '@/store/error.js'
import { useTheming } from '@/store/state'
+import { appMessages } from '@/utils/app-messages'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
import { get_available_capes, get_available_skins } from './helpers/skins'
@@ -289,7 +282,6 @@ const {
} = setupProviders(notificationManager, popupNotificationManager)
const news = ref([])
-const availableSurvey = ref(false)
const displayedServerInviteNotifications = new Set()
const serverInvitePopupNotificationIds = new Set()
let liveNotificationGeneration = 0
@@ -403,6 +395,54 @@ const messages = defineMessages({
id: 'app.ads-consent.accept',
defaultMessage: 'Accept all',
},
+ home: {
+ id: 'app.nav.home',
+ defaultMessage: 'Home',
+ },
+ library: {
+ id: 'app.nav.library',
+ defaultMessage: 'Library',
+ },
+ modrinthHosting: {
+ id: 'app.nav.modrinth-hosting',
+ defaultMessage: 'Modrinth Hosting',
+ },
+ createNewInstance: {
+ id: 'app.nav.create-new-instance',
+ defaultMessage: 'Create new instance',
+ },
+ modrinthAccount: {
+ id: 'app.nav.modrinth-account',
+ defaultMessage: 'Modrinth account',
+ },
+ signedInAs: {
+ id: 'app.nav.signed-in-as',
+ defaultMessage: 'Signed in as {username} ',
+ },
+ signInToModrinthAccount: {
+ id: 'app.nav.sign-in-to-modrinth-account',
+ defaultMessage: 'Sign in to a Modrinth account',
+ },
+ restarting: {
+ id: 'app.restarting',
+ defaultMessage: 'Restarting...',
+ },
+ upgradeToModrinthPlus: {
+ id: 'app.nav.upgrade-to-modrinth-plus',
+ defaultMessage: 'Upgrade to Modrinth+',
+ },
+ news: {
+ id: 'app.news.title',
+ defaultMessage: 'News',
+ },
+ viewAllNews: {
+ id: 'app.news.view-all',
+ defaultMessage: 'View all news',
+ },
+ playingAs: {
+ id: 'app.sidebar.playing-as',
+ defaultMessage: 'Playing as',
+ },
})
function handleAdsConsentRequired(required) {
@@ -571,12 +611,6 @@ async function setupApp() {
settings.pending_update_toast_for_version = null
await setSettings(settings)
}
-
- if (osType === 'windows') {
- await processPendingSurveys()
- } else {
- console.info('Skipping user surveys on non-Windows platforms')
- }
}
const stateFailed = ref(false)
@@ -1439,115 +1473,6 @@ function handleAuxClick(e) {
}
}
-function cleanupOldSurveyDisplayData() {
- const threeWeeksAgo = new Date()
- threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21)
-
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i)
-
- if (key.startsWith('survey-') && key.endsWith('-display')) {
- const dateValue = new Date(localStorage.getItem(key))
- if (dateValue < threeWeeksAgo) {
- localStorage.removeItem(key)
- }
- }
- }
-}
-
-async function openSurvey() {
- if (!availableSurvey.value) {
- console.error('No survey to open')
- return
- }
-
- const creds = await getCreds().catch(handleError)
- const userId = creds?.user_id
-
- const formId = availableSurvey.value.tally_id
-
- const popupOptions = {
- layout: 'modal',
- width: 700,
- autoClose: 2000,
- hideTitle: true,
- hiddenFields: {
- user_id: userId,
- },
- onOpen: () => console.info('Opened user survey'),
- onClose: () => {
- console.info('Closed user survey')
- show_ads_window()
- },
- onSubmit: () => console.info('Active user survey submitted'),
- }
-
- try {
- hide_ads_window()
- if (window.Tally?.openPopup) {
- console.info(`Opening Tally popup for user survey (form ID: ${formId})`)
- dismissSurvey()
- window.Tally.openPopup(formId, popupOptions)
- } else {
- console.warn('Tally script not yet loaded')
- show_ads_window()
- }
- } catch (e) {
- console.error('Error opening Tally popup:', e)
- show_ads_window()
- }
-
- console.info(`Found user survey to show with tally_id: ${formId}`)
- window.Tally.openPopup(formId, popupOptions)
-}
-
-function dismissSurvey() {
- localStorage.setItem(`survey-${availableSurvey.value.id}-display`, new Date())
- availableSurvey.value = undefined
-}
-
-async function processPendingSurveys() {
- function isWithinLastTwoWeeks(date) {
- const twoWeeksAgo = new Date()
- twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14)
- return date >= twoWeeksAgo
- }
-
- cleanupOldSurveyDisplayData()
-
- const creds = await getCreds().catch(handleError)
- const userId = creds?.user_id
-
- const instances = (await list().catch(handleError)) ?? []
- const isActivePlayer = instances.some(
- (instance) =>
- isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
- )
-
- let surveys = []
- try {
- surveys = await $fetch('https://api.modrinth.com/v2/surveys')
- } catch (e) {
- console.error('Error fetching surveys:', e)
- }
-
- const surveyToShow = surveys.find(
- (survey) =>
- !!(
- localStorage.getItem(`survey-${survey.id}-display`) === null &&
- survey.type === 'tally_app' &&
- ((survey.condition === 'active_player' && isActivePlayer) ||
- (survey.assigned_users?.includes(userId) && !survey.dismissed_users?.includes(userId)))
- ),
- )
-
- if (surveyToShow) {
- availableSurvey.value = surveyToShow
- } else {
- console.info('No user survey to show')
- }
-}
-
provideAppUpdateDownloadProgress(appUpdateDownload)
@@ -1570,7 +1495,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
class="flex items-center gap-4 text-contrast font-semibold text-xl select-none cursor-default"
>
- Restarting...
+ {{ formatMessage(messages.restarting) }}
@@ -1593,27 +1518,24 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
/>
-
+
-
-
-
-
-
+
+
-
@@ -1653,7 +1574,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
- Signed in as
-
-
- {{ credentials?.user?.username }}
-
+
+
+
+
+
+
+
+
-
- Sign out
+
+
+ {{ formatMessage(commonMessages.signOutButton) }}
+
@@ -1741,28 +1670,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
}"
>
-
-
-
-
- Would you mind answering a few questions about your experience with Modrinth App?
-
-
- This feedback will go directly to the Modrinth team and help guide future updates!
-
-
-
- Take survey
-
-
- No thanks
-
-
-
-
+
-
-
- {{
- existingPlan?.id === plansByRam.large.id
- ? formatMessage(messages.yourCurrentPlan)
- : formatMessage(messages.selectPlan)
- }}
-
-
+
+ {{
+ existingPlan?.id === plansByRam.large.id
+ ? formatMessage(messages.yourCurrentPlan)
+ : formatMessage(messages.selectPlan)
+ }}
+
-
-
- {{ formatMessage(messages.getStarted) }}
-
-
+
+ {{ formatMessage(messages.getStarted) }}
+
Starting at {{ formatPrice(customStartingPrice, currency, true) }}/mo
diff --git a/packages/ui/src/components/billing/ServersPurchase3Review.vue b/packages/ui/src/components/billing/ServersPurchase3Review.vue
index bfe6096c8d..ce76cbc1af 100644
--- a/packages/ui/src/components/billing/ServersPurchase3Review.vue
+++ b/packages/ui/src/components/billing/ServersPurchase3Review.vue
@@ -15,11 +15,12 @@ import dayjs from 'dayjs'
import type Stripe from 'stripe'
import { computed } from 'vue'
+import { Button } from '#ui/components/base/buttons'
+
import { useFormatPrice } from '../../composables'
import { useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
import { regionOverrides } from '../../utils/regions'
-import ButtonStyled from '../base/ButtonStyled.vue'
import Checkbox from '../base/Checkbox.vue'
import TagItem from '../base/TagItem.vue'
import ModrinthServersIcon from '../servers/ModrinthServersIcon.vue'
@@ -323,12 +324,10 @@ function setInterval(newInterval: ServerBillingInterval) {
No payment method selected
-
-
- Change
- Select payment method
-
-
+
+ Change
+ Select payment method
+
diff --git a/packages/ui/src/components/chart/Chart.vue b/packages/ui/src/components/chart/Chart.vue
index 43c11e1fb5..72d42bc67a 100644
--- a/packages/ui/src/components/chart/Chart.vue
+++ b/packages/ui/src/components/chart/Chart.vue
@@ -4,7 +4,7 @@ import dayjs from 'dayjs'
import { defineAsyncComponent, onMounted, ref } from 'vue'
import { useFormatNumber } from '../../composables/index.ts'
-import Button from '../base/Button.vue'
+import { IconButton } from '../base/buttons'
import Checkbox from '../base/Checkbox.vue'
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
@@ -231,12 +231,16 @@ defineExpose({
diff --git a/packages/ui/src/components/external_files/AddFilesToAttributionGroupModal.vue b/packages/ui/src/components/external_files/AddFilesToAttributionGroupModal.vue
index fc94cf5af1..effcec44fa 100644
--- a/packages/ui/src/components/external_files/AddFilesToAttributionGroupModal.vue
+++ b/packages/ui/src/components/external_files/AddFilesToAttributionGroupModal.vue
@@ -3,7 +3,8 @@ import type { Labrinth } from '@modrinth/api-client'
import { CheckIcon, PlusIcon, SearchIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { computed, nextTick, ref } from 'vue'
-import { ButtonStyled, NewModal, StyledInput } from '#ui/components'
+import { NewModal, StyledInput } from '#ui/components'
+import { Button } from '#ui/components/base/buttons'
import { commonMessages } from '#ui/utils'
import { defineMessages, useVIntl } from '../../composables/i18n'
@@ -263,23 +264,21 @@ defineExpose({ show, hide })
{{ formatMessage(messages.addFilesModalSelectedCount, { count: selectedFileCount }) }}
-
-
-
- {{ formatMessage(commonMessages.cancelButton) }}
-
-
-
-
-
-
- {{ formatMessage(messages.addFilesModalConfirm, { count: selectedFileCount }) }}
-
-
+
+
+ {{ formatMessage(commonMessages.cancelButton) }}
+
+
+
+
+ {{ formatMessage(messages.addFilesModalConfirm, { count: selectedFileCount }) }}
+
diff --git a/packages/ui/src/components/external_files/AddToExistingExternalProjectModal.vue b/packages/ui/src/components/external_files/AddToExistingExternalProjectModal.vue
index 9ad61e3b1a..85ccadcae4 100644
--- a/packages/ui/src/components/external_files/AddToExistingExternalProjectModal.vue
+++ b/packages/ui/src/components/external_files/AddToExistingExternalProjectModal.vue
@@ -4,7 +4,8 @@ import { PlusIcon, SearchIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { useMutation } from '@tanstack/vue-query'
import { computed, ref, useTemplateRef } from 'vue'
-import { Accordion, ButtonStyled, NewModal, StyledInput } from '#ui/components'
+import { Accordion, NewModal, StyledInput } from '#ui/components'
+import { Button } from '#ui/components/base/buttons'
import { injectModrinthClient, injectNotificationManager } from '../../providers'
import AttributionGroupFilePicker from './AttributionGroupFilePicker.vue'
@@ -228,12 +229,15 @@ defineExpose({ show, hide })
wrapper-class="flex-1 min-w-[12rem]"
:disabled="addFilesMutation.isPending.value"
/>
-
-
-
- Search
-
-
+
+
+ Search
+
-
-
- {{ selectedProjectId === project.id ? 'Selected' : 'Select' }}
-
-
+
+ {{ selectedProjectId === project.id ? 'Selected' : 'Select' }}
+
@@ -306,22 +308,29 @@ defineExpose({ show, hide })
/>
-
-
-
- Cancel
-
-
-
-
-
-
- Add files to entry
-
-
+
+
+ Cancel
+
+
+
+
+ Add files to entry
+
diff --git a/packages/ui/src/components/external_files/AddToGlobalPermissionsDatabaseModal.vue b/packages/ui/src/components/external_files/AddToGlobalPermissionsDatabaseModal.vue
index c3a55d80d0..e36d1785d9 100644
--- a/packages/ui/src/components/external_files/AddToGlobalPermissionsDatabaseModal.vue
+++ b/packages/ui/src/components/external_files/AddToGlobalPermissionsDatabaseModal.vue
@@ -4,14 +4,8 @@ import { PlusIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { useMutation } from '@tanstack/vue-query'
import { computed, ref, useTemplateRef } from 'vue'
-import {
- Accordion,
- ButtonStyled,
- Combobox,
- type ComboboxOption,
- NewModal,
- StyledInput,
-} from '#ui/components'
+import { Accordion, Combobox, type ComboboxOption, NewModal, StyledInput } from '#ui/components'
+import { Button } from '#ui/components/base/buttons'
import { injectModrinthClient, injectNotificationManager } from '../../providers'
import AttributionGroupFilePicker from './AttributionGroupFilePicker.vue'
@@ -223,22 +217,26 @@ defineExpose({ show, hide })
-
-
-
- Cancel
-
-
-
-
-
-
- Add to global database
-
-
+
+
+ Cancel
+
+
+
+
+ Add to global database
+
diff --git a/packages/ui/src/components/external_files/AttributionEditor.vue b/packages/ui/src/components/external_files/AttributionEditor.vue
index 3694793a02..82e99ad7cb 100644
--- a/packages/ui/src/components/external_files/AttributionEditor.vue
+++ b/packages/ui/src/components/external_files/AttributionEditor.vue
@@ -14,8 +14,9 @@ import { builtinLicenses } from '@modrinth/utils'
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
-import { ButtonStyled, Chips, Combobox, type ComboboxOption, StyledInput } from '#ui/components'
+import { Chips, Combobox, type ComboboxOption, StyledInput } from '#ui/components'
import { FileInput } from '#ui/components/base'
+import { Button, IconButton } from '#ui/components/base/buttons'
import { commonMessages } from '#ui/utils'
import { defineMessage, defineMessages, useVIntl } from '../../composables/i18n'
@@ -577,15 +578,14 @@ function cancelEditing() {
class="flex w-full object-contain bg-surface-3"
/>
-
-
-
-
-
+
+
+
@@ -633,29 +633,29 @@ function cancelEditing() {
-
-
- {{ formatMessage(commonMessages.cancelButton) }}
-
-
-
-
-
-
- {{ formatMessage(commonMessages.savingButton) }}
-
-
- {{ formatMessage(messages.saveAttribution) }}
-
- {{ formatMessage(messages.addAttribution) }}
-
-
+
+ {{ formatMessage(commonMessages.cancelButton) }}
+
+
+
+
+ {{ formatMessage(commonMessages.savingButton) }}
+
+
+ {{ formatMessage(messages.saveAttribution) }}
+
+ {{ formatMessage(messages.addAttribution) }}
+
diff --git a/packages/ui/src/components/external_files/ExternalProjectLookupCard.vue b/packages/ui/src/components/external_files/ExternalProjectLookupCard.vue
index 4bf6a34a92..2467b5d3cf 100644
--- a/packages/ui/src/components/external_files/ExternalProjectLookupCard.vue
+++ b/packages/ui/src/components/external_files/ExternalProjectLookupCard.vue
@@ -9,7 +9,8 @@ import {
import { Menu } from 'floating-vue'
import { computed } from 'vue'
-import { ButtonStyled, CopyCode } from '#ui/components'
+import { CopyCode } from '#ui/components'
+import { IconButton } from '#ui/components/base/buttons'
import ExternalProjectLicenseStateTag from './ExternalProjectLicenseStateTag.vue'
import type { ExternalLicenseStatus } from './types.ts'
@@ -58,11 +59,16 @@ async function copyProjectLink() {
Project link
-
-
-
-
-
+
+
+
diff --git a/packages/ui/src/components/external_files/ExternalProjectPermissionsCard.vue b/packages/ui/src/components/external_files/ExternalProjectPermissionsCard.vue
index bb26531235..9dc39af921 100644
--- a/packages/ui/src/components/external_files/ExternalProjectPermissionsCard.vue
+++ b/packages/ui/src/components/external_files/ExternalProjectPermissionsCard.vue
@@ -18,8 +18,9 @@ import { renderString } from '@modrinth/utils'
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, useTemplateRef, watch } from 'vue'
-import { ButtonStyled, Collapsible, ConfirmModal, OverflowMenu } from '#ui/components'
+import { Collapsible, ConfirmModal } from '#ui/components'
import type { OverflowMenuOption } from '#ui/components/base'
+import { Button, IconButton, TeleportOverflowMenu } from '#ui/components/base/buttons'
import { commonMessages } from '#ui/utils'
import { defineMessage, defineMessages, useVIntl } from '../../composables/i18n'
@@ -498,6 +499,7 @@ const visibleQuickReplies = computed
(() => {
(reply) =>
({
id: reply.label,
+ label: reply.label,
action: () => handleQuickReply(reply),
}) as OverflowMenuOption,
)
@@ -577,28 +579,27 @@ const visibleQuickReplies = computed(() => {
-
-
-
-
-
-
+
+
+
+
-
-
- {{ formatMessage(messages.addFilesToGroup) }}
-
-
+
+ {{ formatMessage(messages.addFilesToGroup) }}
+
@@ -644,11 +645,9 @@ const visibleQuickReplies = computed(() => {
"
#actions
>
-
-
- {{ formatMessage(commonMessages.editButton) }}
-
-
+
+ {{ formatMessage(commonMessages.editButton) }}
+
-
-
- Reply presets
-
-
-
-
-
-
-
- Approve
-
-
-
-
-
-
- Reject: Insufficient proof
-
-
-
-
-
-
- Reject: Not allowed
-
-
-
-
-
- {{ formatMessage(commonMessages.cancelButton) }}
-
-
+
+ Reply presets
+
+
+
+
+
+ Approve
+
+
+
+
+ Reject: Insufficient proof
+
+
+
+
+ Reject: Not allowed
+
+
+
+ {{ formatMessage(commonMessages.cancelButton) }}
+
-
-
- Add files to database...
-
-
-
-
- Add to existing entry...
-
-
+
+ Add files to database...
+
+
+ Add to existing entry...
+
@@ -808,12 +809,10 @@ const visibleQuickReplies = computed
(() => {
"
class="ml-auto"
>
-
-
-
- {{ formatMessage(commonMessages.editButton) }}
-
-
+
+
+ {{ formatMessage(commonMessages.editButton) }}
+
@@ -835,16 +834,19 @@ const visibleQuickReplies = computed(() => {
/>
-
-
-
-
- {{ formatMessage(messages.removeGroup) }}
-
-
+
+
+
+ {{ formatMessage(messages.removeGroup) }}
+
diff --git a/packages/ui/src/components/flows/creation-flow-modal/components/CustomSetupStage.vue b/packages/ui/src/components/flows/creation-flow-modal/components/CustomSetupStage.vue
index 2d20d1a1a0..9899a8d370 100644
--- a/packages/ui/src/components/flows/creation-flow-modal/components/CustomSetupStage.vue
+++ b/packages/ui/src/components/flows/creation-flow-modal/components/CustomSetupStage.vue
@@ -4,18 +4,14 @@
-
-
-
- {{ formatMessage(messages.selectIcon) }}
-
-
-
-
-
- {{ formatMessage(messages.removeIcon) }}
-
-
+
+
+ {{ formatMessage(messages.selectIcon) }}
+
+
+
+ {{ formatMessage(messages.removeIcon) }}
+
@@ -153,11 +149,11 @@ import { EyeIcon, EyeOffIcon, UploadIcon, XIcon } from '@modrinth/assets'
import { commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
import { computed, onMounted, ref, watch } from 'vue'
+import { Button } from '#ui/components/base/buttons'
import { useDebugLogger } from '#ui/composables/debug-logger'
import { injectFilePicker, injectModrinthClient, injectTags } from '../../../../providers'
import Avatar from '../../../base/Avatar.vue'
-import ButtonStyled from '../../../base/ButtonStyled.vue'
import Chips from '../../../base/Chips.vue'
import Collapsible from '../../../base/Collapsible.vue'
import Combobox, { type ComboboxOption } from '../../../base/Combobox.vue'
diff --git a/packages/ui/src/components/flows/creation-flow-modal/components/ImportInstanceStage.vue b/packages/ui/src/components/flows/creation-flow-modal/components/ImportInstanceStage.vue
index 6351399bd3..3102c9bf78 100644
--- a/packages/ui/src/components/flows/creation-flow-modal/components/ImportInstanceStage.vue
+++ b/packages/ui/src/components/flows/creation-flow-modal/components/ImportInstanceStage.vue
@@ -5,13 +5,14 @@
{{
formatMessage(messages.launcherInstancesTitle)
}}
- {{ formatMessage(messages.clearAll) }}
- {{ formatMessage(messages.clearAll) }}
-
@@ -76,28 +77,22 @@
-
-
- {{ formatMessage(messages.addLauncherPath) }}
-
-
+
+ {{ formatMessage(messages.addLauncherPath) }}
+
-
-
-
-
-
+
+
+
-
-
- {{ formatMessage(messages.add) }}
-
-
+
+ {{ formatMessage(messages.add) }}
+
@@ -108,9 +103,10 @@ import { ChevronRightIcon, FolderSearchIcon, SearchIcon } from '@modrinth/assets
import { defineMessages, useVIntl } from '@modrinth/ui'
import { computed, onMounted, ref, watch } from 'vue'
+import { Button, IconButton } from '#ui/components/base/buttons'
+
import { injectInstanceImport, injectNotificationManager } from '../../../../providers'
import type { ImportableLauncher } from '../../../../providers/instance-import'
-import ButtonStyled from '../../../base/ButtonStyled.vue'
import Checkbox from '../../../base/Checkbox.vue'
import Collapsible from '../../../base/Collapsible.vue'
import StyledInput from '../../../base/StyledInput.vue'
diff --git a/packages/ui/src/components/flows/creation-flow-modal/components/ModpackStage.vue b/packages/ui/src/components/flows/creation-flow-modal/components/ModpackStage.vue
index e0860dd303..8fc5605aa3 100644
--- a/packages/ui/src/components/flows/creation-flow-modal/components/ModpackStage.vue
+++ b/packages/ui/src/components/flows/creation-flow-modal/components/ModpackStage.vue
@@ -30,28 +30,27 @@
-
-
-
- {{ formatMessage(messages.importModpack) }}
-
-
-
-
-
- {{ formatMessage(messages.browseModpacks) }}
-
-
+
+
+ {{ formatMessage(messages.importModpack) }}
+
+
+
+ {{ formatMessage(messages.browseModpacks) }}
+
@@ -61,10 +60,10 @@ import { CompassIcon, ImportIcon, RightArrowIcon } from '@modrinth/assets'
import { commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
import { defineAsyncComponent, h, onMounted, ref, watch } from 'vue'
+import { Button } from '#ui/components/base/buttons'
import { useDebugLogger } from '#ui/composables/debug-logger'
import { injectFilePicker } from '../../../../providers'
-import ButtonStyled from '../../../base/ButtonStyled.vue'
import Combobox from '../../../base/Combobox.vue'
import { injectCreationFlowContext } from '../creation-flow-context'
diff --git a/packages/ui/src/components/modal/ConfirmLeaveModal.vue b/packages/ui/src/components/modal/ConfirmLeaveModal.vue
index cba7b8468c..b49b7c0665 100644
--- a/packages/ui/src/components/modal/ConfirmLeaveModal.vue
+++ b/packages/ui/src/components/modal/ConfirmLeaveModal.vue
@@ -8,18 +8,14 @@
-
-
-
- {{ localizeIfPossible(stayLabel) }}
-
-
-
-
-
- {{ localizeIfPossible(leaveLabel) }}
-
-
+
+
+ {{ localizeIfPossible(stayLabel) }}
+
+
+
+ {{ localizeIfPossible(leaveLabel) }}
+
@@ -30,7 +26,7 @@ import { RightArrowIcon, XIcon } from '@modrinth/assets'
import { ref } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue'
-import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
+import { Button } from '#ui/components/base/buttons'
import { defineMessage, type MessageDescriptor, useVIntl } from '#ui/composables/i18n'
import NewModal from './NewModal.vue'
diff --git a/packages/ui/src/components/modal/ConfirmModal.vue b/packages/ui/src/components/modal/ConfirmModal.vue
index 87a091324e..157b909d20 100644
--- a/packages/ui/src/components/modal/ConfirmModal.vue
+++ b/packages/ui/src/components/modal/ConfirmModal.vue
@@ -31,18 +31,19 @@
wrapper-class="max-w-[20rem]"
/>
-
-
-
- Cancel
-
-
-
-
-
- {{ proceedLabel }}
-
-
+
+
+ Cancel
+
+
+
+ {{ proceedLabel }}
+
@@ -53,7 +54,8 @@ import { TrashIcon, XIcon } from '@modrinth/assets'
import { renderString } from '@modrinth/utils'
import { computed, ref } from 'vue'
-import ButtonStyled from '../base/ButtonStyled.vue'
+import { Button } from '#ui/components/base/buttons'
+
import StyledInput from '../base/StyledInput.vue'
import NewModal from './NewModal.vue'
diff --git a/packages/ui/src/components/modal/NewModal.vue b/packages/ui/src/components/modal/NewModal.vue
index 803b831078..c5cb846bcd 100644
--- a/packages/ui/src/components/modal/NewModal.vue
+++ b/packages/ui/src/components/modal/NewModal.vue
@@ -43,33 +43,28 @@
-
-
-
-
-
+
+
+
-
-
-
-
-
+
+
@@ -116,8 +114,10 @@ import { CheckIcon, DownloadIcon, XIcon } from '@modrinth/assets'
import { commonMessages } from '@modrinth/ui'
import { computed, nextTick, onUnmounted, ref } from 'vue'
+import { Button, ButtonLink } from '#ui/components/base/buttons'
+
import { defineMessages, useVIntl } from '../../composables/i18n'
-import { Avatar, ButtonStyled } from '../base'
+import { Avatar } from '../base'
import ServerOnlinePlayers from '../project/server/ServerOnlinePlayers.vue'
import ServerRegion from '../project/server/ServerRegion.vue'
diff --git a/packages/ui/src/components/modal/ShareModal.vue b/packages/ui/src/components/modal/ShareModal.vue
index db9c485c1b..70cf15203c 100644
--- a/packages/ui/src/components/modal/ShareModal.vue
+++ b/packages/ui/src/components/modal/ShareModal.vue
@@ -12,9 +12,10 @@ import {
import QrcodeVue from 'qrcode.vue'
import { computed, nextTick, ref } from 'vue'
+import { ButtonLink, IconButton } from '#ui/components/base/buttons'
import { injectNotificationManager } from '#ui/providers'
-import { ButtonStyled, NewModal, StyledInput } from '../index'
+import { NewModal, StyledInput } from '../index'
const props = defineProps({
header: {
@@ -156,16 +157,15 @@ defineExpose({
-
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
-
-
- Open in new tab
-
-
-
+
+ Open in new tab
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/src/components/modal/UnknownFileWarningModal.vue b/packages/ui/src/components/modal/UnknownFileWarningModal.vue
index 247ff96c04..cd4d6055b4 100644
--- a/packages/ui/src/components/modal/UnknownFileWarningModal.vue
+++ b/packages/ui/src/components/modal/UnknownFileWarningModal.vue
@@ -86,17 +86,13 @@
/>
-
-
- {{ formatMessage(messages.installAnyway) }}
-
-
-
-
-
- {{ formatMessage(messages.dontInstall) }}
-
-
+
+ {{ formatMessage(messages.installAnyway) }}
+
+
+
+ {{ formatMessage(messages.dontInstall) }}
+
@@ -106,10 +102,11 @@
import { BanIcon } from '@modrinth/assets'
import { computed, nextTick, ref, useTemplateRef } from 'vue'
+import { Button } from '#ui/components/base/buttons'
+
import { defineMessages, useVIntl } from '../../composables/i18n'
import { useScrollIndicator } from '../../composables/scroll-indicator'
import Admonition from '../base/Admonition.vue'
-import ButtonStyled from '../base/ButtonStyled.vue'
import Checkbox from '../base/Checkbox.vue'
import Table, { type TableColumn } from '../base/Table.vue'
import NewModal from './NewModal.vue'
diff --git a/packages/ui/src/components/nav/NotificationPanel.vue b/packages/ui/src/components/nav/NotificationPanel.vue
index 95626d907e..0f0a592d53 100644
--- a/packages/ui/src/components/nav/NotificationPanel.vue
+++ b/packages/ui/src/components/nav/NotificationPanel.vue
@@ -59,22 +59,27 @@
x{{ item.count }}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
- {{ button.label }}
-
-
+
+ {{ button.label }}
+
@@ -121,6 +132,7 @@ import {
} from '@modrinth/assets'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
+import { Button, IconButton } from '#ui/components/base/buttons'
import { useModalStack } from '#ui/composables/modal-stack.ts'
import {
@@ -128,7 +140,6 @@ import {
type WebNotification,
type WebNotificationButton,
} from '../../providers'
-import ButtonStyled from '../base/ButtonStyled.vue'
const notificationManager = injectNotificationManager()
const notifications = computed(() => notificationManager.getNotifications())
diff --git a/packages/ui/src/components/nav/PopupNotificationPanel.vue b/packages/ui/src/components/nav/PopupNotificationPanel.vue
index 49c55ccd58..da57f93c20 100644
--- a/packages/ui/src/components/nav/PopupNotificationPanel.vue
+++ b/packages/ui/src/components/nav/PopupNotificationPanel.vue
@@ -95,11 +95,15 @@
-
-
-
-
-
+
+
+
{{ item.text }}
@@ -142,16 +146,28 @@
full-width
/>
-
-
-
- {{ btn.label }}
-
-
+
+ {{ btn.label }}
+
@@ -171,6 +187,8 @@ import {
} from '@modrinth/assets'
import { computed, ref } from 'vue'
+import { Button, IconButton } from '#ui/components/base/buttons'
+
import { useModalStack } from '../../composables/modal-stack'
import {
injectPopupNotificationManager,
@@ -178,7 +196,6 @@ import {
type PopupNotificationButton,
type PopupNotificationProgressItem,
} from '../../providers'
-import ButtonStyled from '../base/ButtonStyled.vue'
import ProgressBar from '../base/ProgressBar.vue'
import NotificationToast from '../notifications/NotificationToast.vue'
diff --git a/packages/ui/src/components/notifications/NotificationToast.vue b/packages/ui/src/components/notifications/NotificationToast.vue
index 185fee06d7..82169e0044 100644
--- a/packages/ui/src/components/notifications/NotificationToast.vue
+++ b/packages/ui/src/components/notifications/NotificationToast.vue
@@ -49,31 +49,33 @@
-
-
-
-
-
+
+
+
-
-
-
-
- Accept
-
-
-
-
-
- Decline
-
-
+
+
+
+ Accept
+
+
+
+ Decline
+
@@ -96,16 +98,17 @@
{{ entityLabel }}
-
-
-
-
-
+
+
+
-
- Launch game
-
-
- Instance
-
+ Launch game
+ Instance
{{ progressLabel }}
@@ -145,16 +144,28 @@
v-if="type === 'instance-download' && actions?.length"
class="col-start-1 col-end-3 row-start-3 mt-2 flex min-w-0 flex-wrap items-center gap-2"
>
-
-
-
- {{ action.label }}
-
-
+
+ {{ action.label }}
+
@@ -182,11 +193,12 @@
import { CheckIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { computed, ref } from 'vue'
+import { Button, IconButton } from '#ui/components/base/buttons'
+
import { useFormatBytes, useFormatNumber } from '../../composables'
import type { PopupNotificationButton, PopupNotificationProgressType } from '../../providers'
import { truncatedTooltip } from '../../utils/truncate'
import Avatar from '../base/Avatar.vue'
-import ButtonStyled from '../base/ButtonStyled.vue'
type NotificationToastType =
| 'friend-request'
diff --git a/packages/ui/src/components/page/NormalPage.vue b/packages/ui/src/components/page/NormalPage.vue
index ce3f0e1689..3acc18993e 100644
--- a/packages/ui/src/components/page/NormalPage.vue
+++ b/packages/ui/src/components/page/NormalPage.vue
@@ -3,6 +3,7 @@ import { injectPageContext } from '@modrinth/ui'
defineProps<{
sidebar?: 'right' | 'left'
+ fullWidth?: boolean
}>()
const { hierarchicalSidebarAvailable } = injectPageContext()
@@ -12,6 +13,7 @@ const { hierarchicalSidebarAvailable } = injectPageContext()
:class="{
'ui-normal-page--sidebar-left': sidebar === 'left' && !hierarchicalSidebarAvailable,
'ui-normal-page--sidebar-right': sidebar === 'right' && !hierarchicalSidebarAvailable,
+ 'ui-normal-page--full-width': fullWidth,
}"
>
-
-
-
-
-
+
+
+
-
-
diff --git a/packages/ui/src/layouts/shared/server-settings/pages/installation.vue b/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
index 3d943bd823..21275870a7 100644
--- a/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
+++ b/packages/ui/src/layouts/shared/server-settings/pages/installation.vue
@@ -22,17 +22,16 @@
formatMessage(messages.resetServerTitle)
}}
-
-
-
- {{ formatMessage(commonMessages.resetServerButton) }}
-
-
+
+
+ {{ formatMessage(commonMessages.resetServerButton) }}
+
{{ formatMessage(messages.resetServerDescription) }}
@@ -59,17 +58,16 @@
{{ formatMessage(messages.supportOptionsTitle) }}
-
-
-
- {{ formatMessage(messages.resetToOnboardingButton) }}
-
-
+
+
+ {{ formatMessage(messages.resetToOnboardingButton) }}
+
@@ -79,7 +77,6 @@
import type { Archon } from '@modrinth/api-client'
import { RotateCounterClockwiseIcon } from '@modrinth/assets'
import {
- ButtonStyled,
commonMessages,
ConfirmModal,
defineMessages,
@@ -103,6 +100,7 @@ import {
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, useTemplateRef, watch } from 'vue'
+import { Button } from '#ui/components/base/buttons'
import { injectFilePicker } from '#ui/providers/file-picker'
const debug = useDebugLogger('LoaderPage')
diff --git a/packages/ui/src/layouts/shared/server-settings/pages/network.vue b/packages/ui/src/layouts/shared/server-settings/pages/network.vue
index dd8091b289..9a91d08000 100644
--- a/packages/ui/src/layouts/shared/server-settings/pages/network.vue
+++ b/packages/ui/src/layouts/shared/server-settings/pages/network.vue
@@ -16,18 +16,16 @@
placeholder="e.g. Secondary allocation"
/>
-
- Cancel
-
-
-
- Update allocation
-
-
+ Cancel
+
+ Update allocation
+
@@ -61,9 +59,14 @@
allocationsError?.message ?? 'Unknown error'
}}
- refetchAllocations()">
- Retry
-
+ refetchAllocations()"
+ >Retry
@@ -83,16 +86,16 @@
placeholder="e.g. Secondary allocation"
/>
-
-
-
- Create allocation
-
-
+
+
+ Create allocation
+
@@ -105,30 +108,33 @@
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -153,16 +159,14 @@
:placeholder="exampleDomain"
/>
-
-
-
- Export
-
-
+
+
+ Export
+
@@ -223,8 +227,9 @@ import {
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, nextTick, ref } from 'vue'
-import { ButtonStyled, ConfirmModal, NewModal, StyledInput, Table, TagItem } from '#ui/components'
+import { ConfirmModal, NewModal, StyledInput, Table, TagItem } from '#ui/components'
import type { TableColumn } from '#ui/components/base'
+import { Button, IconButton } from '#ui/components/base/buttons'
import { useServerPermissions } from '#ui/composables/server-permissions'
import {
injectModrinthClient,
diff --git a/packages/ui/src/layouts/shared/user-profile/layout.vue b/packages/ui/src/layouts/shared/user-profile/layout.vue
index 9c3b008a6b..608994520e 100644
--- a/packages/ui/src/layouts/shared/user-profile/layout.vue
+++ b/packages/ui/src/layouts/shared/user-profile/layout.vue
@@ -13,19 +13,26 @@
-
-
-
- {{ formatMessage(commonMessages.cancelButton) }}
-
-
-
-
-
-
- {{ formatMessage(messages.blockButton) }}
-
-
+
+
+ {{ formatMessage(commonMessages.cancelButton) }}
+
+
+
+
+ {{ formatMessage(messages.blockButton) }}
+
@@ -42,28 +49,26 @@
:placeholder="formatMessage(messages.selectRolePlaceholder)"
/>
-
-
-
- {{ formatMessage(commonMessages.cancelButton) }}
-
-
-
-
-
-
- {{ formatMessage(messages.savingLabel) }}
-
-
-
- {{ formatMessage(commonMessages.saveChangesButton) }}
-
-
-
+
+
+ {{ formatMessage(commonMessages.cancelButton) }}
+
+
+
+
+ {{ formatMessage(messages.savingLabel) }}
+
+
+
+ {{ formatMessage(commonMessages.saveChangesButton) }}
+
+
@@ -192,7 +197,7 @@
-
+
+ >
+
+
+
+
-
-
- {{ formatMessage(messages.createProjectButton) }}
-
-
+
+ {{ formatMessage(messages.createProjectButton) }}
+
@@ -380,11 +387,9 @@
"
>
-
-
- {{ formatMessage(messages.createCollectionButton) }}
-
-
+
+ {{ formatMessage(messages.createCollectionButton) }}
+
@@ -437,11 +442,9 @@
:description="formatMessage(messages.userLoadErrorDescription)"
>
-
-
- {{ formatMessage(commonMessages.retryButton) }}
-
-
+
+ {{ formatMessage(commonMessages.retryButton) }}
+
@@ -473,7 +476,7 @@ import { useRoute, useRouter } from 'vue-router'
import Admonition from '#ui/components/base/Admonition.vue'
import AutoLink from '#ui/components/base/AutoLink.vue'
import Avatar from '#ui/components/base/Avatar.vue'
-import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
+import { Button } from '#ui/components/base/buttons'
import Combobox from '#ui/components/base/Combobox.vue'
import EmptyState from '#ui/components/base/EmptyState.vue'
import IntlFormatted from '#ui/components/base/IntlFormatted.vue'
diff --git a/packages/ui/src/layouts/wrapped/AccountProfileSettings.vue b/packages/ui/src/layouts/wrapped/AccountProfileSettings.vue
index 1cd295cdcc..f38537a444 100644
--- a/packages/ui/src/layouts/wrapped/AccountProfileSettings.vue
+++ b/packages/ui/src/layouts/wrapped/AccountProfileSettings.vue
@@ -15,12 +15,10 @@
-
-
-
- {{ formatMessage(commonMessages.signInButton) }}
-
-
+
+
+ {{ formatMessage(commonMessages.signInButton) }}
+
@@ -56,30 +54,33 @@
-
-
-
-
-
-
-
-
- {{ formatMessage(commonMessages.removeImageButton) }}
-
-
-
-
-
- {{ formatMessage(commonMessages.resetButton) }}
-
-
+
+
+
+
+
+ {{ formatMessage(commonMessages.removeImageButton) }}
+
+
+
+ {{ formatMessage(commonMessages.resetButton) }}
+
@@ -135,9 +136,8 @@ import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import { RouterLink } from 'vue-router'
import Avatar from '#ui/components/base/Avatar.vue'
-import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
+import { Button, FileButton } from '#ui/components/base/buttons'
import EmptyState from '#ui/components/base/EmptyState.vue'
-import FileInput from '#ui/components/base/FileInput.vue'
import IntlFormatted from '#ui/components/base/IntlFormatted.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import { defineMessages, useVIntl } from '#ui/composables'
diff --git a/packages/ui/src/layouts/wrapped/AccountSocialSettings.vue b/packages/ui/src/layouts/wrapped/AccountSocialSettings.vue
index 6d2f4f5d8c..240744fa31 100644
--- a/packages/ui/src/layouts/wrapped/AccountSocialSettings.vue
+++ b/packages/ui/src/layouts/wrapped/AccountSocialSettings.vue
@@ -15,12 +15,10 @@
-
-
-
- {{ formatMessage(commonMessages.signInButton) }}
-
-
+
+
+ {{ formatMessage(commonMessages.signInButton) }}
+
@@ -112,11 +110,9 @@
{{ formatMessage(messages.loadError) }}
-
-
- {{ formatMessage(commonMessages.retryButton) }}
-
-
+
+ {{ formatMessage(commonMessages.retryButton) }}
+
{{ formatMessage(messages.noBlockedUsers) }}
@@ -145,25 +141,24 @@
-
-
-
- {{ formatMessage(messages.unblockButton) }}
-
-
+
+
+ {{ formatMessage(messages.unblockButton) }}
+
@@ -195,7 +190,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import Avatar from '#ui/components/base/Avatar.vue'
-import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
+import { Button } from '#ui/components/base/buttons'
import Chips from '#ui/components/base/Chips.vue'
import EmptyState from '#ui/components/base/EmptyState.vue'
import Table, { type TableColumn } from '#ui/components/base/Table.vue'
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/access/access.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/access/access.vue
index 96cc63944a..b85f8d8fe3 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/access/access.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/access/access.vue
@@ -14,23 +14,25 @@
v-model="roleFilter"
:options="roleFilterOptions"
:display-value="selectedRoleFilterLabel"
- trigger-class="min-w-[225px] !h-10 !min-h-10 !py-0"
+ trigger-size="lg"
+ trigger-class="min-w-[225px]"
>
-
-
-
- {{ formatMessage(messages.inviteFriends) }}
-
-
+
+
+ {{ formatMessage(messages.inviteFriends) }}
+
@@ -113,7 +115,7 @@ import { FilterIcon, SearchIcon, UserPlusIcon } from '@modrinth/assets'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
-import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
+import { Button } from '#ui/components/base/buttons'
import Combobox, { type ComboboxOption } from '#ui/components/base/Combobox.vue'
import DropdownFilterBar from '#ui/components/base/DropdownFilterBar.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue
index bcdebf6392..f231ba7956 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/onboarding.vue
@@ -42,22 +42,22 @@
-
-
-
- {{ formatMessage(messages.uploadingProgress, { percent: uploadPercent }) }}
-
-
-
-
- {{ formatMessage(messages.setupServerButton) }}
-
-
+
+
+ {{ formatMessage(messages.uploadingProgress, { percent: uploadPercent }) }}
+
+
+ {{ formatMessage(messages.setupServerButton) }}
+
{{ error.message }}
-
- Retry
-
+ Retry
@@ -71,16 +71,16 @@
{{ formatMessage(commonMessages.allProjectType) }}
-
-
-
- {{ formatMessage(messages.createBackup) }}
-
-
+
+
+ {{ formatMessage(messages.createBackup) }}
+
@@ -95,17 +95,17 @@
:description="formatMessage(messages.emptyDescription)"
>
-
-
-
- {{ formatMessage(messages.createBackup) }}
-
-
+
+
+ {{ formatMessage(messages.createBackup) }}
+
-
-
- {{ formatMessage(messages.clearFilters) }}
-
-
+
+ {{ formatMessage(messages.clearFilters) }}
+
@@ -198,30 +196,30 @@
}}
-
-
- {{ formatMessage(commonMessages.clearButton) }}
-
-
+
+ {{ formatMessage(commonMessages.clearButton) }}
+
-
-
-
- {{ formatMessage(commonMessages.deleteLabel) }}
-
-
+
+
+ {{ formatMessage(commonMessages.deleteLabel) }}
+
@@ -273,7 +271,7 @@ import type { Component } from 'vue'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
-import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
+import { Button } from '#ui/components/base/buttons'
import Checkbox from '#ui/components/base/Checkbox.vue'
import EmptyState from '#ui/components/base/EmptyState.vue'
import FilterPills, { type FilterPillOption } from '#ui/components/base/FilterPills.vue'
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/index.vue b/packages/ui/src/layouts/wrapped/hosting/manage/index.vue
index d22ac2dac4..f85d23337c 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/index.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/index.vue
@@ -75,14 +75,17 @@
-
- {{
- formatMessage(messages.contactSupportButton)
- }}
-
- router.go(0)">
- {{ formatMessage(messages.reloadButton) }}
-
+ {{ formatMessage(messages.contactSupportButton) }}
+ router.go(0)">{{
+ formatMessage(messages.reloadButton)
+ }}
@@ -106,12 +109,10 @@
:placeholder="formatMessage(messages.searchPlaceholder, { count: filteredData.length })"
wrapper-class="w-full md:w-72"
/>
-
-
-
- {{ formatMessage(messages.newServerButton) }}
-
-
+
+
+ {{ formatMessage(messages.newServerButton) }}
+
@@ -231,8 +232,6 @@
import type { Archon, Labrinth } from '@modrinth/api-client'
import { HammerIcon, LoaderCircleIcon, PlusIcon, SearchIcon } from '@modrinth/assets'
import {
- AutoLink,
- ButtonStyled,
CopyCode,
defineMessages,
injectAuth,
@@ -255,6 +254,7 @@ import type Stripe from 'stripe'
import { type ComponentPublicInstance, computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
+import { Button, ButtonLink } from '#ui/components/base/buttons'
import ServersUpgradeModalWrapper from '#ui/components/billing/ServersUpgradeModalWrapper.vue'
import type { ServerListingOwner } from '#ui/components/servers/access'
import MedalServerListing from '#ui/components/servers/marketing/MedalServerListing.vue'
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
index c781570242..dbf56d1b69 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
@@ -181,31 +181,30 @@
:auto-hide="false"
placement="bottom-end"
>
-
-
-
-
-
+
+
+
{{ formatMessage(settingsHintMessages.title) }}
-
-
-
-
-
+
+
+
{{ formatMessage(settingsHintMessages.description) }}
@@ -213,15 +212,15 @@
-
-
-
-
-
+
+
+
@@ -284,13 +283,11 @@
If you're stuck, please contact Modrinth Support with the information below:
-
-
-
-
- Copy Debug Info
-
-
+
+
+
+ Copy Debug Info
+
An internal error occurred while installing your server. Don't fret — try
@@ -310,25 +307,23 @@
v-if="errorTitle === 'Installation error'"
class="mt-2 flex flex-col gap-4 sm:flex-row"
>
-
- Open Installation Log
-
-
-
-
-
- Copy Debug Info
-
-
-
-
-
- Change Loader
-
-
+ Open Installation Log
+
+
+
+ Copy Debug Info
+
+
+
+ Change Loader
+
@@ -430,7 +425,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import Avatar from '#ui/components/base/Avatar.vue'
-import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
+import { Button, IconButton, TeleportOverflowMenu } from '#ui/components/base/buttons'
import ErrorInformationCard from '#ui/components/base/ErrorInformationCard.vue'
import NavTabs from '#ui/components/base/NavTabs.vue'
import PageHeader from '#ui/components/base/page-header/index.vue'
@@ -438,7 +433,6 @@ import PageHeaderMetadata from '#ui/components/base/page-header/metadata/index.v
import PageHeaderMetadataItem from '#ui/components/base/page-header/metadata/page-header-metadata-item.vue'
import PageHeaderActions from '#ui/components/base/page-header/page-header-actions.vue'
import ServerNotice from '#ui/components/base/ServerNotice.vue'
-import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue'
import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
import ServerPanelAdmonitions from '#ui/components/servers/admonitions/ServerPanelAdmonitions.vue'
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'
diff --git a/packages/ui/src/stories/add-stories.md b/packages/ui/src/stories/add-stories.md
index 19f83c3ba9..2132d4f14c 100644
--- a/packages/ui/src/stories/add-stories.md
+++ b/packages/ui/src/stories/add-stories.md
@@ -153,16 +153,14 @@ For components that need user interaction to show:
```typescript
export const Default: Story = {
render: () => ({
- components: { NewModal, ButtonStyled },
+ components: { Button, NewModal },
setup() {
const modalRef = ref | null>(null)
return { modalRef }
},
template: /* html */ `
-
- Open Modal
-
+
Open Modal
Modal content
@@ -199,10 +197,10 @@ Components should use relative imports, not the package alias:
```typescript
// ❌ BAD - Causes circular dependency in Storybook
-import { ButtonStyled } from '@modrinth/ui'
+import { Button } from '@modrinth/ui'
// ✅ GOOD - Use relative imports
-import ButtonStyled from '../base/ButtonStyled.vue'
+import Button from '../components/base/buttons/Button.vue'
```
### 2. Object/Array Prop Defaults Must Be Factory Functions
diff --git a/packages/ui/src/stories/base/Admonition.stories.ts b/packages/ui/src/stories/base/Admonition.stories.ts
index 413e2a1b6d..bcd6894084 100644
--- a/packages/ui/src/stories/base/Admonition.stories.ts
+++ b/packages/ui/src/stories/base/Admonition.stories.ts
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import Admonition from '../../components/base/Admonition.vue'
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
+import { Button } from '../../components/base/buttons'
const meta = {
title: 'Base/Admonition',
@@ -78,7 +78,7 @@ export const HeaderWithTimestamp: Story = {
export const WithTopRightActions: Story = {
render: () => ({
- components: { Admonition, ButtonStyled },
+ components: { Admonition, Button },
template: /*html*/ `
Uploading server files...
-
- Cancel
-
+ Cancel
Something went wrong while extracting the archive.
-
- Retry
-
+ Retry
@@ -115,7 +111,7 @@ export const WithTopRightActions: Story = {
export const WithProgressBar: Story = {
render: () => ({
- components: { Admonition, ButtonStyled },
+ components: { Admonition, Button },
template: /*html*/ `
128 KB / 1.2 MB (45%)
-
- Cancel
-
+ Cancel
24 MB extracted — config/settings.yml
-
- Cancel
-
+ Cancel
({
- components: { Button },
- setup() {
- return { args }
- },
- template: /*html*/ `
- Click me
- `,
- }),
-} satisfies Meta
-
-export default meta
-type Story = StoryObj
-
-export const Default: Story = {}
-
-export const Primary: Story = {
- args: {
- color: 'primary',
- },
-}
-
-export const Danger: Story = {
- args: {
- color: 'danger',
- },
-}
-
-export const AllColors: Story = {
- render: () => ({
- components: { Button },
- template: /*html*/ `
-
- Default
- Primary
- Danger
- Red
- Orange
- Green
- Blue
- Purple
-
- `,
- }),
-}
-
-export const Large: Story = {
- args: {
- large: true,
- },
-}
-
-export const Outline: Story = {
- args: {
- outline: true,
- },
-}
-
-export const Transparent: Story = {
- args: {
- transparent: true,
- },
-}
-
-export const Disabled: Story = {
- args: {
- disabled: true,
- },
-}
-
-export const AsLink: Story = {
- args: {
- link: 'https://modrinth.com',
- external: true,
- },
-}
diff --git a/packages/ui/src/stories/base/ButtonStyled.stories.ts b/packages/ui/src/stories/base/ButtonStyled.stories.ts
deleted file mode 100644
index 852ceab118..0000000000
--- a/packages/ui/src/stories/base/ButtonStyled.stories.ts
+++ /dev/null
@@ -1,226 +0,0 @@
-import { DownloadIcon, HeartIcon, SettingsIcon } from '@modrinth/assets'
-import type { Meta, StoryObj } from '@storybook/vue3-vite'
-
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
-
-const colors = ['standard', 'brand', 'red', 'orange', 'green', 'blue', 'purple'] as const
-const types = [
- 'standard',
- 'outlined',
- 'transparent',
- 'highlight',
- 'highlight-colored-text',
- 'chip',
-] as const
-const sizes = ['small', 'standard', 'large'] as const
-
-const meta = {
- title: 'Base/ButtonStyled',
- component: ButtonStyled,
- argTypes: {
- color: {
- control: 'select',
- options: [...colors, 'medal-promo'],
- },
- size: {
- control: 'select',
- options: [...sizes],
- },
- type: {
- control: 'select',
- options: [...types],
- },
- circular: { control: 'boolean' },
- colorFill: {
- control: 'select',
- options: ['auto', 'background', 'text', 'none'],
- },
- hoverColorFill: {
- control: 'select',
- options: ['auto', 'background', 'text', 'none'],
- },
- highlighted: { control: 'boolean' },
- highlightedStyle: {
- control: 'select',
- options: ['main-nav-primary', 'main-nav-secondary'],
- },
- },
- args: {
- color: 'standard',
- size: 'standard',
- type: 'standard',
- circular: false,
- colorFill: 'auto',
- hoverColorFill: 'auto',
- highlighted: false,
- highlightedStyle: 'main-nav-primary',
- },
- render: (args) => ({
- components: { ButtonStyled, DownloadIcon },
- setup() {
- return { args }
- },
- template: /*html*/ `
-
- Button
-
- `,
- }),
-} satisfies Meta
-
-export default meta
-type Story = StoryObj
-
-export const Default: Story = {
- args: {
- type: 'standard',
- },
-}
-
-export const AllVariants: Story = {
- render: () => ({
- components: { ButtonStyled },
- setup() {
- return { colors, types }
- },
- template: /*html*/ `
-
-
-
-
- Color / Type
- {{ type }}
-
-
-
-
- {{ color }}
-
-
- Button
-
-
-
-
-
-
- `,
- }),
-}
-
-export const AllVariantsHighlighted: Story = {
- render: () => ({
- components: { ButtonStyled },
- setup() {
- return { colors, types }
- },
- template: /*html*/ `
-
-
-
-
- Color / Type
- {{ type }}
-
-
-
-
- {{ color }}
-
-
- Button
-
-
-
-
-
-
- `,
- }),
-}
-
-export const Sizes: Story = {
- render: () => ({
- components: { ButtonStyled },
- setup() {
- return { sizes, types }
- },
- template: /*html*/ `
-
-
-
-
- Size / Type
- {{ type }}
-
-
-
-
- {{ size }}
-
-
- Button
-
-
-
-
-
-
- `,
- }),
-}
-
-export const WithIcons: Story = {
- render: () => ({
- components: { ButtonStyled, DownloadIcon, HeartIcon, SettingsIcon },
- setup() {
- return { types }
- },
- template: /*html*/ `
-
-
-
-
- Variant
- {{ type }}
-
-
-
-
- Icon + text
-
-
- Download
-
-
-
-
- Icon only
-
-
-
-
-
-
-
-
-
- `,
- }),
-}
-
-export const Disabled: Story = {
- render: () => ({
- components: { ButtonStyled },
- setup() {
- return { types }
- },
- template: /*html*/ `
-
-
- {{ type }}
-
-
- `,
- }),
-}
diff --git a/packages/ui/src/stories/base/EmptyState.stories.ts b/packages/ui/src/stories/base/EmptyState.stories.ts
index f698148b9b..8fa5be7f23 100644
--- a/packages/ui/src/stories/base/EmptyState.stories.ts
+++ b/packages/ui/src/stories/base/EmptyState.stories.ts
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
+import { Button } from '../../components/base/buttons'
import EmptyState from '../../components/base/EmptyState.vue'
const meta = {
@@ -42,7 +42,7 @@ export const Default: Story = {
export const WithActions: StoryObj = {
render: () => ({
- components: { EmptyState, ButtonStyled },
+ components: { EmptyState, Button },
template: /*html*/ `
-
- Create backup
-
+ Create backup
`,
diff --git a/packages/ui/src/stories/base/JoinedButtons.stories.ts b/packages/ui/src/stories/base/JoinedButtons.stories.ts
deleted file mode 100644
index c007e3631d..0000000000
--- a/packages/ui/src/stories/base/JoinedButtons.stories.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { PlayIcon, SlashIcon, StopCircleIcon, UpdatedIcon } from '@modrinth/assets'
-import type { Meta, StoryObj } from '@storybook/vue3-vite'
-
-import JoinedButtons from '../../components/base/JoinedButtons.vue'
-
-const meta = {
- title: 'Base/JoinedButtons',
- component: JoinedButtons,
- argTypes: {
- color: {
- control: 'select',
- options: ['standard', 'brand', 'red', 'orange', 'green', 'blue', 'purple'],
- },
- size: {
- control: 'select',
- options: ['small', 'standard', 'large'],
- },
- disabled: { control: 'boolean' },
- primaryDisabled: { control: 'boolean' },
- dropdownDisabled: { control: 'boolean' },
- primaryMuted: { control: 'boolean' },
- },
-} satisfies Meta
-
-export default meta
-type Story = StoryObj
-
-export const Start: Story = {
- args: {
- color: 'brand',
- size: 'large',
- actions: [
- {
- id: 'start',
- label: 'Start',
- icon: PlayIcon,
- action: () => console.log('Start'),
- },
- ],
- },
-}
-
-export const StopWithKill: Story = {
- args: {
- color: 'red',
- size: 'large',
- actions: [
- {
- id: 'stop',
- label: 'Stop',
- icon: StopCircleIcon,
- action: () => console.log('Stop'),
- },
- {
- id: 'kill_server',
- label: 'Kill server',
- icon: SlashIcon,
- action: () => console.log('Kill'),
- },
- ],
- },
-}
-
-export const Stopping: Story = {
- args: {
- color: 'red',
- size: 'large',
- primaryDisabled: true,
- primaryMuted: true,
- actions: [
- {
- id: 'stop',
- label: 'Stopping',
- icon: StopCircleIcon,
- action: () => console.log('Stop'),
- },
- {
- id: 'kill_server',
- label: 'Kill server',
- icon: SlashIcon,
- action: () => console.log('Kill'),
- },
- ],
- },
-}
-
-export const Restart: Story = {
- args: {
- color: 'orange',
- size: 'large',
- actions: [
- {
- id: 'restart',
- label: 'Restart',
- icon: UpdatedIcon,
- action: () => console.log('Restart'),
- },
- ],
- },
-}
-
-export const Disabled: Story = {
- args: {
- color: 'red',
- size: 'large',
- disabled: true,
- actions: [
- {
- id: 'stop',
- label: 'Stop',
- icon: StopCircleIcon,
- action: () => console.log('Stop'),
- },
- {
- id: 'kill_server',
- label: 'Kill server',
- icon: SlashIcon,
- action: () => console.log('Kill'),
- },
- ],
- },
-}
diff --git a/packages/ui/src/stories/base/OverflowMenu.stories.ts b/packages/ui/src/stories/base/OverflowMenu.stories.ts
deleted file mode 100644
index 0adec75cf2..0000000000
--- a/packages/ui/src/stories/base/OverflowMenu.stories.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { MoreHorizontalIcon } from '@modrinth/assets'
-import type { Meta, StoryObj } from '@storybook/vue3-vite'
-
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
-import OverflowMenu from '../../components/base/OverflowMenu.vue'
-
-const meta = {
- title: 'Base/OverflowMenu',
- component: OverflowMenu,
- render: (args) => ({
- components: { OverflowMenu, MoreHorizontalIcon, ButtonStyled },
- setup() {
- return { args }
- },
- template: /*html*/ `
-
-
-
- Edit
- Delete
- Share
-
-
- `,
- }),
-} satisfies Meta
-
-export default meta
-type Story = StoryObj
-
-export const Default: Story = {
- args: {
- options: [
- { id: 'edit', action: () => console.log('Edit clicked') },
- { id: 'share', action: () => console.log('Share clicked') },
- { divider: true },
- { id: 'delete', action: () => console.log('Delete clicked'), color: 'danger' },
- ],
- },
-}
-
-export const WithDifferentPlacements: StoryObj = {
- render: () => ({
- components: { OverflowMenu, MoreHorizontalIcon, ButtonStyled },
- template: /*html*/ `
-
-
- bottom-end (default)
-
-
-
- Edit
- Delete
-
-
-
-
- bottom-start
-
-
-
- Edit
- Delete
-
-
-
-
- `,
- }),
-}
diff --git a/packages/ui/src/stories/base/PageHeader.stories.ts b/packages/ui/src/stories/base/PageHeader.stories.ts
index 73ad628820..d2e569ae6b 100644
--- a/packages/ui/src/stories/base/PageHeader.stories.ts
+++ b/packages/ui/src/stories/base/PageHeader.stories.ts
@@ -20,9 +20,13 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import AutoLink from '../../components/base/AutoLink.vue'
import Avatar from '../../components/base/Avatar.vue'
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
+import {
+ Button,
+ IconButton,
+ SplitButton,
+ TeleportOverflowMenu,
+} from '../../components/base/buttons'
import FormattedTag from '../../components/base/FormattedTag.vue'
-import JoinedButtons from '../../components/base/JoinedButtons.vue'
import PageHeader from '../../components/base/page-header/index.vue'
import PageHeaderMetadata from '../../components/base/page-header/metadata/index.vue'
import PageHeaderMetadataItem from '../../components/base/page-header/metadata/page-header-metadata-item.vue'
@@ -32,7 +36,6 @@ import PageHeaderMetadataTimeItem from '../../components/base/page-header/metada
import PageHeaderActions from '../../components/base/page-header/page-header-actions.vue'
import PageHeaderBadgeItem from '../../components/base/page-header/page-header-badge-item.vue'
import TagItem from '../../components/base/TagItem.vue'
-import TeleportOverflowMenu from '../../components/base/TeleportOverflowMenu.vue'
import LoaderIcon from '../../components/servers/icons/LoaderIcon.vue'
import ServerIcon from '../../components/servers/icons/ServerIcon.vue'
@@ -87,9 +90,10 @@ const pageHeaderIcons = {
const pageHeaderComponents = {
AutoLink,
Avatar,
- ButtonStyled,
+ Button,
FormattedTag,
- JoinedButtons,
+ IconButton,
+ SplitButton,
PageHeader,
PageHeaderActions,
PageHeaderBadgeItem,
@@ -111,7 +115,7 @@ const meta = {
},
decorators: [
(story) => ({
- components: { story },
+ components: { story, TeleportOverflowMenu },
template: '
',
}),
],
@@ -157,17 +161,13 @@ export const ProjectHeader: Story = {
-
-
-
- Download
-
-
-
-
-
-
-
+
+
+ Download
+
+
+
+
@@ -207,12 +207,10 @@ export const CreatorHeader: Story = {
-
-
-
- Follow
-
-
+
+
+ Follow
+
@@ -222,10 +220,7 @@ export const CreatorHeader: Story = {
export const AppInstanceHeader: Story = {
render: () => ({
- components: {
- ...pageHeaderComponents,
- LoaderIcon,
- },
+ components: { ...pageHeaderComponents, LoaderIcon, TeleportOverflowMenu },
setup() {
return {
...pageHeaderIcons,
@@ -252,22 +247,16 @@ export const AppInstanceHeader: Story = {
-
-
-
- Play
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Play
+
+
+
+
+
+
+
@@ -277,10 +266,7 @@ export const AppInstanceHeader: Story = {
export const BrowseHeader: Story = {
render: () => ({
- components: {
- ...pageHeaderComponents,
- LoaderIcon,
- },
+ components: { ...pageHeaderComponents, LoaderIcon, TeleportOverflowMenu },
setup() {
return {
...pageHeaderIcons,
@@ -291,11 +277,9 @@ export const BrowseHeader: Story = {
template: `
-
-
-
-
-
+
+
+
@@ -317,10 +301,7 @@ export const BrowseHeader: Story = {
export const ServerPanelRootHeader: Story = {
render: () => ({
- components: {
- ...pageHeaderComponents,
- ServerIcon,
- },
+ components: { ...pageHeaderComponents, ServerIcon, TeleportOverflowMenu },
setup() {
return {
...pageHeaderIcons,
@@ -346,17 +327,13 @@ export const ServerPanelRootHeader: Story = {
-
-
-
- Start server
-
-
-
-
-
-
-
+
+
+ Start server
+
+
+
+
@@ -366,10 +343,7 @@ export const ServerPanelRootHeader: Story = {
export const ServerPanelInstanceHeader: Story = {
render: () => ({
- components: {
- ...pageHeaderComponents,
- LoaderIcon,
- },
+ components: { ...pageHeaderComponents, LoaderIcon, TeleportOverflowMenu },
setup() {
return {
...pageHeaderIcons,
@@ -381,11 +355,9 @@ export const ServerPanelInstanceHeader: Story = {
template: `
-
-
-
-
-
+
+
+
@@ -400,18 +372,24 @@ export const ServerPanelInstanceHeader: Story = {
-
-
-
- Start instance
-
-
-
-
-
-
-
-
+
+
+ Start instance
+
+
+
+ Stop
+
+
+
+
diff --git a/packages/ui/src/stories/base/PopoutMenu.stories.ts b/packages/ui/src/stories/base/PopoutMenu.stories.ts
deleted file mode 100644
index 7e3d02a512..0000000000
--- a/packages/ui/src/stories/base/PopoutMenu.stories.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { SettingsIcon } from '@modrinth/assets'
-import type { Meta, StoryObj } from '@storybook/vue3-vite'
-
-import Button from '../../components/base/Button.vue'
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
-import PopoutMenu from '../../components/base/PopoutMenu.vue'
-
-const meta = {
- title: 'Base/PopoutMenu',
- component: PopoutMenu,
- render: (args) => ({
- components: { PopoutMenu, Button, ButtonStyled, SettingsIcon },
- setup() {
- return { args }
- },
- template: /*html*/ `
-
-
-
-
-
- Option 1
- Option 2
- Option 3
-
-
-
-
- `,
- }),
-} satisfies Meta
-
-export default meta
-type Story = StoryObj
-
-export const Default: Story = {}
-
-export const WithTooltip: Story = {
- args: {
- tooltip: 'Click for more options',
- },
-}
-
-export const DifferentPlacements: StoryObj = {
- render: () => ({
- components: { PopoutMenu, Button, ButtonStyled, SettingsIcon },
- template: /*html*/ `
-
-
-
bottom-end (default)
-
-
-
-
-
- Option 1
- Option 2
-
-
-
-
-
-
-
bottom-start
-
-
-
-
-
- Option 1
- Option 2
-
-
-
-
-
-
-
top-end
-
-
-
-
-
- Option 1
- Option 2
-
-
-
-
-
-
- `,
- }),
-}
diff --git a/packages/ui/src/stories/base/StackedAdmonitions.stories.ts b/packages/ui/src/stories/base/StackedAdmonitions.stories.ts
index c0683ad7df..fbef68fb42 100644
--- a/packages/ui/src/stories/base/StackedAdmonitions.stories.ts
+++ b/packages/ui/src/stories/base/StackedAdmonitions.stories.ts
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import Admonition from '../../components/base/Admonition.vue'
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
+import { Button } from '../../components/base/buttons'
import StackedAdmonitionsRaw, {
type StackedAdmonitionItem,
} from '../../components/base/StackedAdmonitions.vue'
@@ -383,7 +383,7 @@ interface RichItem extends StackedAdmonitionItem {
export const RichContent: Story = {
render: () => ({
- components: { StackedAdmonitions, Admonition, ButtonStyled },
+ components: { StackedAdmonitions, Admonition, Button },
setup() {
const items = ref([
{
@@ -429,12 +429,8 @@ export const RichContent: Story = {
>
{{ item.body }}
-
- Cancel
-
-
- Retry
-
+ Cancel
+ Retry
diff --git a/packages/ui/src/stories/base/Table.stories.ts b/packages/ui/src/stories/base/Table.stories.ts
index 157c1ffbcb..6448cbe7bd 100644
--- a/packages/ui/src/stories/base/Table.stories.ts
+++ b/packages/ui/src/stories/base/Table.stories.ts
@@ -3,8 +3,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { computed, ref } from 'vue'
import Badge from '../../components/base/Badge.vue'
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
-import OverflowMenu from '../../components/base/OverflowMenu.vue'
+import { Button, TeleportOverflowMenu } from '../../components/base/buttons'
import Table from '../../components/base/Table.vue'
interface User {
@@ -54,7 +53,7 @@ export default meta
export const Default: StoryObj = {
args: {},
render: () => ({
- components: { Table },
+ components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -74,7 +73,7 @@ export const Default: StoryObj = {
export const HorizontalOverflow: StoryObj = {
args: {},
render: () => ({
- components: { Table },
+ components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -103,7 +102,7 @@ export const HorizontalOverflow: StoryObj = {
export const CustomClasses: StoryObj = {
args: {},
render: () => ({
- components: { Table },
+ components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name', cellClass: '!overflow-visible py-3' },
@@ -129,7 +128,7 @@ export const CustomClasses: StoryObj = {
export const WithSelection: StoryObj = {
args: {},
render: () => ({
- components: { Table },
+ components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -160,7 +159,7 @@ export const WithSelection: StoryObj = {
export const WithSelectionData: StoryObj = {
args: {},
render: () => ({
- components: { Table },
+ components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -193,7 +192,7 @@ export const WithSelectionData: StoryObj = {
export const WithSelectionIds: StoryObj = {
args: {},
render: () => ({
- components: { Table },
+ components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -233,7 +232,7 @@ export const WithSorting: StoryObj = {
},
},
render: () => ({
- components: { Table },
+ components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name', enableSorting: true },
@@ -269,7 +268,7 @@ export const WithSorting: StoryObj = {
export const WithColumnAlignment: StoryObj = {
args: {},
render: () => ({
- components: { Table },
+ components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name', align: 'left' as const },
@@ -289,7 +288,7 @@ export const WithColumnAlignment: StoryObj = {
export const WithCustomCellSlots: StoryObj = {
args: {},
render: () => ({
- components: { Table, Badge },
+ components: { Table, Badge, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -335,7 +334,7 @@ export const WithCustomCellSlots: StoryObj = {
export const WithCustomHeaderSlots: StoryObj = {
args: {},
render: () => ({
- components: { Table },
+ components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -365,7 +364,7 @@ export const WithCustomHeaderSlots: StoryObj = {
export const WithHeaderSlot: StoryObj = {
args: {},
render: () => ({
- components: { Table, ButtonStyled },
+ components: { Table, Button, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -383,9 +382,7 @@ export const WithHeaderSlot: StoryObj = {
Team Members
-
- Invite member
-
+ Invite member
@@ -397,7 +394,7 @@ export const WithHeaderSlot: StoryObj = {
export const WithActionsColumn: StoryObj = {
args: {},
render: () => ({
- components: { Table, ButtonStyled, EditIcon, TrashIcon },
+ components: { Table, EditIcon, TrashIcon, Button, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -421,18 +418,14 @@ export const WithActionsColumn: StoryObj = {
-
-
-
- Edit
-
-
-
-
-
- Delete
-
-
+
+
+ Edit
+
+
+
+ Delete
+
@@ -443,7 +436,7 @@ export const WithActionsColumn: StoryObj = {
export const WithLocalizedActionsColumn: StoryObj = {
args: {},
render: () => ({
- components: { Table, ButtonStyled, EditIcon, TrashIcon },
+ components: { Table, EditIcon, TrashIcon, Button, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Nombre' },
@@ -467,18 +460,14 @@ export const WithLocalizedActionsColumn: StoryObj = {
-
-
-
- Editar
-
-
-
-
-
- Eliminar
-
-
+
+
+ Editar
+
+
+
+ Eliminar
+
@@ -489,7 +478,7 @@ export const WithLocalizedActionsColumn: StoryObj = {
export const FullFeatured: StoryObj = {
args: {},
render: () => ({
- components: { Table, Badge, ButtonStyled, EditIcon, TrashIcon },
+ components: { Table, Badge, EditIcon, TrashIcon, Button, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name', enableSorting: true },
@@ -565,18 +554,14 @@ export const FullFeatured: StoryObj = {
-
-
-
- Edit
-
-
-
-
-
- Delete
-
-
+
+
+ Edit
+
+
+
+ Delete
+
@@ -592,7 +577,7 @@ export const FullFeatured: StoryObj = {
export const VirtualizedLargeData: StoryObj = {
args: {},
render: () => ({
- components: { Table, Badge },
+ components: { Table, Badge, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name', enableSorting: true },
@@ -710,7 +695,15 @@ export const VirtualizedLargeData: StoryObj = {
export const WithOverflowMenu: StoryObj = {
args: {},
render: () => ({
- components: { Table, Badge, ButtonStyled, OverflowMenu, MoreVerticalIcon, EditIcon, TrashIcon },
+ components: {
+ Table,
+ Badge,
+ MoreVerticalIcon,
+ EditIcon,
+ TrashIcon,
+ Button,
+ TeleportOverflowMenu,
+ },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -737,17 +730,19 @@ export const WithOverflowMenu: StoryObj = {
const getMenuOptions = (row: User) => [
{
id: 'edit',
+ label: 'Edit',
action: () => alert(`Edit user: ${row.name}`),
},
{
id: 'duplicate',
+ label: 'Duplicate',
action: () => alert(`Duplicate user: ${row.name}`),
},
- { divider: true },
+ { type: 'divider' },
{
id: 'delete',
- color: 'red' as const,
- hoverFilled: true,
+ label: 'Delete',
+ tone: 'red',
action: () => alert(`Delete user: ${row.name}`),
},
]
@@ -766,26 +761,23 @@ export const WithOverflowMenu: StoryObj = {
-
-
-
-
-
- Edit
-
-
-
- Duplicate
-
-
-
- Delete
-
-
-
+
+
+
+ Edit
+
+
+
+ Duplicate
+
+
+
+ Delete
+
+
@@ -796,7 +788,7 @@ export const WithOverflowMenu: StoryObj = {
export const EmptyState: StoryObj = {
args: {},
render: () => ({
- components: { Table },
+ components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
diff --git a/packages/ui/src/stories/buttons/Button.stories.ts b/packages/ui/src/stories/buttons/Button.stories.ts
new file mode 100644
index 0000000000..85bfe95332
--- /dev/null
+++ b/packages/ui/src/stories/buttons/Button.stories.ts
@@ -0,0 +1,213 @@
+import { DownloadIcon, ExternalIcon, HeartIcon, SettingsIcon } from '@modrinth/assets'
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import Button from '../../components/base/buttons/Button.vue'
+import ButtonLink from '../../components/base/buttons/ButtonLink.vue'
+import IconButton from '../../components/base/buttons/IconButton.vue'
+
+const types = ['base', 'colored', 'outlined', 'quiet'] as const
+const sizes = ['sm', 'md', 'lg', 'xl'] as const
+const colors = ['brand', 'red', 'orange', 'green', 'blue', 'purple', 'medal_promotion'] as const
+const sizeColumns = [
+ { value: 'sm', label: 'Small' },
+ { value: 'md', label: 'Medium' },
+ { value: 'lg', label: 'Large' },
+ { value: 'xl', label: 'Extra large' },
+] as const
+const typeRows = [
+ { label: 'Base', type: 'base' },
+ { label: 'Outlined', type: 'outlined' },
+ { label: 'Quiet', type: 'quiet' },
+ ...colors.map((color) => ({
+ label: `Colored / ${color.charAt(0).toUpperCase()}${color.slice(1)}`,
+ type: 'colored' as const,
+ color,
+ })),
+ ...colors.map((color) => ({
+ label: `Outlined / ${color.charAt(0).toUpperCase()}${color.slice(1)}`,
+ type: 'outlined' as const,
+ color,
+ })),
+ ...colors.map((color) => ({
+ label: `Quiet / ${color.charAt(0).toUpperCase()}${color.slice(1)}`,
+ type: 'quiet' as const,
+ color,
+ })),
+]
+
+const meta = {
+ title: 'Buttons/Button',
+ component: Button,
+ argTypes: {
+ type: {
+ control: 'select',
+ options: types,
+ },
+ size: {
+ control: 'select',
+ options: sizes,
+ },
+ color: {
+ control: 'select',
+ options: colors,
+ },
+ nativeType: {
+ control: 'select',
+ options: ['button', 'submit', 'reset'],
+ },
+ disabled: { control: 'boolean' },
+ loading: { control: 'boolean' },
+ },
+ args: {
+ type: 'base',
+ size: 'md',
+ color: 'brand',
+ nativeType: 'button',
+ disabled: false,
+ loading: false,
+ },
+ render: (args) => ({
+ components: { Button, DownloadIcon },
+ setup() {
+ return { args }
+ },
+ template: /*html*/ `
+
+
+ Download
+
+ `,
+ }),
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Playground: Story = {}
+
+export const AllTypes: Story = {
+ render: () => ({
+ components: { Button, DownloadIcon },
+ setup() {
+ return { sizeColumns, typeRows }
+ },
+ template: /*html*/ `
+
+
+
+ {{ size.label }}
+
+
+
+ {{ row.label }}
+
+ Button
+
+
+
+ `,
+ }),
+}
+
+export const Quiet: Story = {
+ render: () => ({
+ components: { Button, DownloadIcon, IconButton, SettingsIcon },
+ template: /*html*/ `
+
+ Quiet
+ Quiet destructive
+
+
+ `,
+ }),
+}
+
+export const Sizes: Story = {
+ render: () => ({
+ components: { Button, DownloadIcon, IconButton },
+ setup() {
+ return { sizes }
+ },
+ template: /*html*/ `
+
+
+ {{ size }}
+
+
+
+ `,
+ }),
+}
+
+export const Colors: Story = {
+ render: () => ({
+ components: { Button },
+ setup() {
+ return { colors }
+ },
+ template: /*html*/ `
+
+
+ {{ color }}
+
+
+ `,
+ }),
+}
+
+export const Content: Story = {
+ render: () => ({
+ components: { Button, DownloadIcon, SettingsIcon },
+ template: /*html*/ `
+
+ Text only
+ Leading icon
+ Trailing icon
+ Full width
+ Continue with a deliberately long translated action label
+
+ `,
+ }),
+}
+
+export const InteractionStates: Story = {
+ render: () => ({
+ components: { Button },
+ template: /*html*/ `
+
+ Enabled
+ Disabled
+ Loading
+ Colored
+ Colored disabled
+ Outlined
+ Quiet
+
+ `,
+ }),
+}
+
+export const LinksAndIconButton: Story = {
+ render: () => ({
+ components: { ButtonLink, ExternalIcon, HeartIcon, IconButton },
+ template: /*html*/ `
+
+ Internal link
+
+ Modrinth
+
+ Disabled link
+
+
+
+
+
+ `,
+ }),
+}
diff --git a/packages/ui/src/stories/buttons/ButtonGroup.stories.ts b/packages/ui/src/stories/buttons/ButtonGroup.stories.ts
new file mode 100644
index 0000000000..3f234148c4
--- /dev/null
+++ b/packages/ui/src/stories/buttons/ButtonGroup.stories.ts
@@ -0,0 +1,82 @@
+import { PlayIcon, SettingsIcon, StopCircleIcon, TrashIcon } from '@modrinth/assets'
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import Button from '../../components/base/buttons/Button.vue'
+import ButtonGroup from '../../components/base/buttons/ButtonGroup.vue'
+import SplitButton from '../../components/base/buttons/SplitButton.vue'
+import type { OverflowMenuOption } from '../../components/base/buttons/types'
+
+const splitOptions: OverflowMenuOption[] = [
+ {
+ id: 'settings',
+ label: 'Server settings',
+ icon: SettingsIcon,
+ action: () => undefined,
+ },
+ { type: 'divider' },
+ {
+ id: 'delete',
+ label: 'Delete server',
+ icon: TrashIcon,
+ tone: 'red',
+ action: () => undefined,
+ },
+]
+
+const meta = {
+ title: 'Buttons/Button Group',
+ component: ButtonGroup,
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Joined: Story = {
+ render: () => ({
+ components: { Button, ButtonGroup },
+ template: /*html*/ `
+
+ Previous
+ Next
+
+ `,
+ }),
+}
+
+export const Split: Story = {
+ render: () => ({
+ components: { PlayIcon, SplitButton },
+ setup() {
+ return { splitOptions }
+ },
+ template: /*html*/ `
+
+ Start server
+
+ `,
+ }),
+}
+
+export const IndependentDisabledStates: Story = {
+ render: () => ({
+ components: { SplitButton, StopCircleIcon },
+ setup() {
+ return { splitOptions }
+ },
+ template: /*html*/ `
+
+
+ Primary disabled
+
+
+ Menu disabled
+
+
+ `,
+ }),
+}
diff --git a/packages/ui/src/stories/buttons/FileButton.stories.ts b/packages/ui/src/stories/buttons/FileButton.stories.ts
new file mode 100644
index 0000000000..2f07b27acc
--- /dev/null
+++ b/packages/ui/src/stories/buttons/FileButton.stories.ts
@@ -0,0 +1,61 @@
+import { UploadIcon } from '@modrinth/assets'
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import FileButton from '../../components/base/buttons/FileButton.vue'
+
+const meta = {
+ title: 'Buttons/File Button',
+ component: FileButton,
+ argTypes: {
+ type: {
+ control: 'select',
+ options: ['base', 'colored', 'outlined', 'quiet'],
+ },
+ size: {
+ control: 'select',
+ options: ['sm', 'md', 'lg', 'xl'],
+ },
+ color: {
+ control: 'select',
+ options: ['brand', 'red', 'orange', 'green', 'blue', 'purple', 'medal_promotion'],
+ },
+ },
+ args: {
+ prompt: 'Select file',
+ type: 'base',
+ size: 'md',
+ multiple: false,
+ disabled: false,
+ },
+ render: (args) => ({
+ components: { FileButton, UploadIcon },
+ setup() {
+ return { args }
+ },
+ template: /*html*/ `
+
+
+
+ `,
+ }),
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {}
+
+export const MultipleImages: Story = {
+ args: {
+ prompt: 'Select images',
+ accept: 'image/*',
+ multiple: true,
+ type: 'colored',
+ },
+}
+
+export const Disabled: Story = {
+ args: {
+ disabled: true,
+ },
+}
diff --git a/packages/ui/src/stories/buttons/TeleportOverflowMenu.stories.ts b/packages/ui/src/stories/buttons/TeleportOverflowMenu.stories.ts
new file mode 100644
index 0000000000..5ed21392f1
--- /dev/null
+++ b/packages/ui/src/stories/buttons/TeleportOverflowMenu.stories.ts
@@ -0,0 +1,104 @@
+import {
+ DownloadIcon,
+ ExternalIcon,
+ MoreVerticalIcon,
+ SettingsIcon,
+ TrashIcon,
+} from '@modrinth/assets'
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import TeleportOverflowMenu from '../../components/base/buttons/TeleportOverflowMenu.vue'
+import type { OverflowMenuOption } from '../../components/base/buttons/types'
+
+const options: OverflowMenuOption[] = [
+ {
+ id: 'download',
+ label: 'Download',
+ icon: DownloadIcon,
+ action: () => undefined,
+ },
+ {
+ id: 'settings',
+ label: 'Project settings',
+ icon: SettingsIcon,
+ type: 'link',
+ to: '/settings',
+ },
+ {
+ id: 'website',
+ label: 'Open website',
+ icon: ExternalIcon,
+ type: 'link',
+ href: 'https://modrinth.com',
+ target: '_blank',
+ },
+ {
+ id: 'unavailable',
+ label: 'Unavailable action',
+ disabled: true,
+ tooltip: 'This action is currently unavailable',
+ action: () => undefined,
+ },
+ { type: 'divider' },
+ {
+ id: 'delete',
+ label: 'Delete project',
+ icon: TrashIcon,
+ tone: 'red',
+ action: () => undefined,
+ },
+]
+
+const meta = {
+ title: 'Buttons/Teleport Overflow Menu',
+ component: TeleportOverflowMenu,
+ args: {
+ label: 'More actions',
+ options,
+ type: 'base',
+ size: 'md',
+ placement: 'bottom-end',
+ disabled: false,
+ hoverable: false,
+ },
+ render: (args) => ({
+ components: { MoreVerticalIcon, TeleportOverflowMenu },
+ setup() {
+ return { args }
+ },
+ template: /*html*/ `
+
+
+
+ `,
+ }),
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {}
+
+export const ColoredTrigger: Story = {
+ args: {
+ type: 'colored',
+ },
+}
+
+export const OutlinedTrigger: Story = {
+ args: {
+ type: 'outlined',
+ },
+}
+
+export const QuietTrigger: Story = {
+ args: {
+ type: 'quiet',
+ },
+}
+
+export const Hoverable: Story = {
+ args: {
+ hoverable: true,
+ },
+}
diff --git a/packages/ui/src/stories/buttons/TeleportPopoutMenu.stories.ts b/packages/ui/src/stories/buttons/TeleportPopoutMenu.stories.ts
new file mode 100644
index 0000000000..d6b183197c
--- /dev/null
+++ b/packages/ui/src/stories/buttons/TeleportPopoutMenu.stories.ts
@@ -0,0 +1,50 @@
+import { SettingsIcon } from '@modrinth/assets'
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import Button from '../../components/base/buttons/Button.vue'
+import TeleportPopoutMenu from '../../components/base/buttons/TeleportPopoutMenu.vue'
+
+const meta = {
+ title: 'Buttons/Teleport Popout Menu',
+ component: TeleportPopoutMenu,
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const ArbitraryContent: Story = {
+ render: () => ({
+ components: { Button, SettingsIcon, TeleportPopoutMenu },
+ template: /*html*/ `
+
+
+ Configure
+
+
+
+
+
+ Version name
+
+
+ Apply
+
+
+
+ `,
+ }),
+}
+
+export const IconTrigger: Story = {
+ render: () => ({
+ components: { SettingsIcon, TeleportPopoutMenu },
+ template: /*html*/ `
+
+
+
+ Arbitrary teleported content can live here.
+
+
+ `,
+ }),
+}
diff --git a/packages/ui/src/stories/instances/ContentCardTable.stories.ts b/packages/ui/src/stories/instances/ContentCardTable.stories.ts
index 486a4c909e..e7a90ebbea 100644
--- a/packages/ui/src/stories/instances/ContentCardTable.stories.ts
+++ b/packages/ui/src/stories/instances/ContentCardTable.stories.ts
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { fn } from 'storybook/test'
import { onMounted, onUnmounted, ref } from 'vue'
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
+import { Button, IconButton } from '../../components/base/buttons'
import ContentCardTable from '../../layouts/shared/content-tab/components/ContentCardTable.vue'
import type { ContentCardTableItem } from '../../layouts/shared/content-tab/types'
@@ -539,7 +539,7 @@ export const InteractiveActions: Story = {
export const WithCustomItemButtons: Story = {
render: () => ({
- components: { ContentCardTable, ButtonStyled, EyeIcon, FolderOpenIcon, DownloadIcon },
+ components: { ContentCardTable, EyeIcon, FolderOpenIcon, DownloadIcon, Button, IconButton },
setup() {
return { items: sampleItems }
},
@@ -551,23 +551,17 @@ export const WithCustomItemButtons: Story = {
@delete="(id) => console.log('Delete', id)"
>
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
`,
@@ -582,15 +576,13 @@ export const WithEmptyState: Story = {
export const WithCustomEmptyState: Story = {
render: () => ({
- components: { ContentCardTable, ButtonStyled },
+ components: { ContentCardTable, Button, IconButton },
template: /*html*/ `
No mods installed
-
- Browse mods
-
+ Browse mods
@@ -781,7 +773,7 @@ export const WithOverflowMenu: Story = {
export const BulkActionsDemo: Story = {
render: () => ({
- components: { ContentCardTable, ButtonStyled },
+ components: { ContentCardTable, Button, IconButton },
setup() {
const items = ref([
{ ...sodiumItem, enabled: true },
@@ -825,15 +817,9 @@ export const BulkActionsDemo: Story = {
{{ selectedIds.length }} selected
-
- Enable
-
-
- Disable
-
-
- Delete
-
+ Enable
+ Disable
+ Delete
export const InstanceDependency: Story = {
render: () => ({
- components: { ButtonStyled, ContentDependencyWarningModal },
+ components: { ContentDependencyWarningModal, Button },
setup() {
const modalRef = ref | null>(null)
const deleted = ref(false)
@@ -186,9 +186,7 @@ export const InstanceDependency: Story = {
},
template: /* html */ `
-
- Delete dependency
-
+
Delete dependency
Dependency deletion confirmed
({
- components: { ButtonStyled, ContentDependencyWarningModal },
+ components: { ContentDependencyWarningModal, Button },
setup() {
const modalRef = ref | null>(null)
const deleted = ref(false)
@@ -227,9 +225,7 @@ export const ServerDependency: Story = {
},
template: /* html */ `
-
- Delete server dependency
-
+
Delete server dependency
Server dependency deletion confirmed
({
- components: { ButtonStyled, ContentDependencyWarningModal },
+ components: { ContentDependencyWarningModal, Button },
setup() {
const modalRef = ref | null>(null)
const deleted = ref(false)
@@ -267,9 +263,7 @@ export const BulkDependencies: Story = {
},
template: /* html */ `
-
- Delete selected dependencies
-
+
Delete selected dependencies
Bulk dependency deletion confirmed
export const ModExample: Story = {
render: (args) => ({
- components: { ContentUpdaterModal, ButtonStyled },
+ components: { ContentUpdaterModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show()
@@ -277,9 +277,7 @@ export const ModExample: Story = {
},
template: /*html*/ `
-
- Update Sodium
-
+
Update Sodium
({
- components: { ContentUpdaterModal, ButtonStyled },
+ components: { ContentUpdaterModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show()
@@ -318,9 +316,7 @@ export const ModpackExample: Story = {
},
template: /*html*/ `
-
- Update Cobblemon Modpack
-
+
Update Cobblemon Modpack
({
- components: { ContentUpdaterModal, ButtonStyled },
+ components: { ContentUpdaterModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show()
@@ -355,9 +351,7 @@ export const WithIncompatibleVersions: Story = {
},
template: /*html*/ `
-
- Update (Shows Incompatible)
-
+
Update (Shows Incompatible)
({
- components: { ContentUpdaterModal, ButtonStyled },
+ components: { ContentUpdaterModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show()
@@ -389,9 +383,7 @@ export const AllVersionTypes: Story = {
},
template: /*html*/ `
-
- View All Version Types
-
+
View All Version Types
export const Default: Story = {
render: () => ({
- components: { ModpackContentModal, ButtonStyled },
+ components: { ModpackContentModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show(mixedModpackContent)
@@ -417,9 +417,7 @@ export const Default: Story = {
},
template: /*html*/ `
-
- View Modpack Content (Mixed)
-
+
View Modpack Content (Mixed)
({
- components: { ModpackContentModal, ButtonStyled },
+ components: { ModpackContentModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show(modsOnlyContent)
@@ -440,9 +438,7 @@ export const ModsOnly: Story = {
},
template: /*html*/ `
-
- View Modpack Content (Mods Only)
-
+ View Modpack Content (Mods Only)
`,
@@ -455,7 +451,7 @@ export const ModsOnly: Story = {
export const LoadingState: Story = {
render: () => ({
- components: { ModpackContentModal, ButtonStyled },
+ components: { ModpackContentModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => {
@@ -469,9 +465,7 @@ export const LoadingState: Story = {
},
template: /*html*/ `
-
- View Content (With Loading)
-
+
View Content (With Loading)
({
- components: { ModpackContentModal, ButtonStyled },
+ components: { ModpackContentModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show([])
@@ -496,9 +490,7 @@ export const EmptyContent: Story = {
},
template: /*html*/ `
-
- View Empty Modpack
-
+ View Empty Modpack
`,
@@ -511,7 +503,7 @@ export const EmptyContent: Story = {
export const LargeModpack: Story = {
render: () => ({
- components: { ModpackContentModal, ButtonStyled },
+ components: { ModpackContentModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show(largeModpackContent)
@@ -519,9 +511,7 @@ export const LargeModpack: Story = {
},
template: /*html*/ `
-
- View Large Modpack (47 items)
-
+
View Large Modpack (47 items)
({
- components: { ModpackContentModal, ButtonStyled },
+ components: { ModpackContentModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show(mixedModpackContent)
@@ -549,9 +539,7 @@ export const SearchDemo: Story = {
Click the button and try searching for "sodium", "shader", or "faithful" to test the search functionality.
-
- Test Search
-
+ Test Search
`,
@@ -564,7 +552,7 @@ export const SearchDemo: Story = {
export const FilterDemo: Story = {
render: () => ({
- components: { ModpackContentModal, ButtonStyled },
+ components: { ModpackContentModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show(mixedModpackContent)
@@ -575,9 +563,7 @@ export const FilterDemo: Story = {
Click the button and try the filter chips (Mods, Shaders, Resource Packs) to filter content by type.
-
- Test Filters
-
+ Test Filters
`,
@@ -590,7 +576,7 @@ export const FilterDemo: Story = {
export const MixedOwnerTypes: Story = {
render: () => ({
- components: { ModpackContentModal, ButtonStyled },
+ components: { ModpackContentModal, Button },
setup() {
const modalRef = ref | null>(null)
// Mix of user and organization owners
@@ -608,9 +594,7 @@ export const MixedOwnerTypes: Story = {
Shows content with different owner types: users (circular avatar) and organizations (rounded + icon).
-
- View Mixed Owners
-
+ View Mixed Owners
`,
diff --git a/packages/ui/src/stories/modal/ConfirmLeaveModal.stories.ts b/packages/ui/src/stories/modal/ConfirmLeaveModal.stories.ts
index 9509d27ac1..ac33eb8886 100644
--- a/packages/ui/src/stories/modal/ConfirmLeaveModal.stories.ts
+++ b/packages/ui/src/stories/modal/ConfirmLeaveModal.stories.ts
@@ -1,7 +1,7 @@
import type { StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
+import { Button } from '../../components/base/buttons'
import ConfirmLeaveModal from '../../components/modal/ConfirmLeaveModal.vue'
const meta = {
@@ -14,7 +14,7 @@ type Story = StoryObj
export const Default: Story = {
render: () => ({
- components: { ConfirmLeaveModal, ButtonStyled },
+ components: { ConfirmLeaveModal, Button },
setup() {
const modalRef = ref | null>(null)
const result = ref('')
@@ -27,9 +27,7 @@ export const Default: Story = {
},
template: /* html */ `
-
- Trigger Leave Confirmation
-
+
Trigger Leave Confirmation
{{ result }}
@@ -39,7 +37,7 @@ export const Default: Story = {
export const CustomMessages: Story = {
render: () => ({
- components: { ConfirmLeaveModal, ButtonStyled },
+ components: { ConfirmLeaveModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.prompt()
@@ -47,9 +45,7 @@ export const CustomMessages: Story = {
},
template: /* html */ `
-
- Discard Draft?
-
+
Discard Draft?
({
- components: { ConfirmLeaveModal, ButtonStyled },
+ components: { ConfirmLeaveModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.prompt()
@@ -73,9 +69,7 @@ export const WarningAdmonition: Story = {
},
template: /* html */ `
-
- Open Warning Variant
-
+
Open Warning Variant
export const Default: Story = {
render: () => ({
- components: { NewModal, ButtonStyled },
+ components: { NewModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show()
@@ -22,9 +22,7 @@ export const Default: Story = {
},
template: `
-
- Open Modal
-
+
Open Modal
This is the modal content.
You can put any content here.
@@ -36,7 +34,7 @@ export const Default: Story = {
export const WithActions: Story = {
render: () => ({
- components: { NewModal, ButtonStyled },
+ components: { NewModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show()
@@ -44,19 +42,13 @@ export const WithActions: Story = {
},
template: `
-
- Open Modal with Actions
-
+
Open Modal with Actions
Are you sure you want to proceed with this action?
-
- Cancel
-
-
- Confirm
-
+ Cancel
+ Confirm
@@ -67,7 +59,7 @@ export const WithActions: Story = {
export const DangerFade: Story = {
render: () => ({
- components: { NewModal, ButtonStyled },
+ components: { NewModal, Button },
setup() {
const modalRef = ref
| null>(null)
const openModal = () => modalRef.value?.show()
@@ -75,19 +67,13 @@ export const DangerFade: Story = {
},
template: `
-
- Open Danger Modal
-
+
Open Danger Modal
Are you sure you want to delete this item? This action cannot be undone.
-
- Cancel
-
-
- Delete
-
+ Cancel
+ Delete
@@ -98,7 +84,7 @@ export const DangerFade: Story = {
export const WarningFade: Story = {
render: () => ({
- components: { NewModal, ButtonStyled },
+ components: { NewModal, Button },
setup() {
const modalRef = ref
| null>(null)
const openModal = () => modalRef.value?.show()
@@ -106,19 +92,13 @@ export const WarningFade: Story = {
},
template: `
-
- Open Warning Modal
-
+
Open Warning Modal
This action may have unintended consequences. Please review before proceeding.
-
- Cancel
-
-
- Proceed
-
+ Cancel
+ Proceed
@@ -129,7 +109,7 @@ export const WarningFade: Story = {
export const Scrollable: Story = {
render: () => ({
- components: { NewModal, ButtonStyled },
+ components: { NewModal, Button },
setup() {
const modalRef = ref
| null>(null)
const openModal = () => modalRef.value?.show()
@@ -137,9 +117,7 @@ export const Scrollable: Story = {
},
template: `
-
- Open Scrollable Modal
-
+
Open Scrollable Modal
@@ -148,9 +126,7 @@ export const Scrollable: Story = {
-
- Close
-
+ Close
@@ -161,7 +137,7 @@ export const Scrollable: Story = {
export const MergedHeader: Story = {
render: () => ({
- components: { NewModal, ButtonStyled },
+ components: { NewModal, Button },
setup() {
const modalRef = ref
| null>(null)
const openModal = () => modalRef.value?.show()
@@ -169,9 +145,7 @@ export const MergedHeader: Story = {
},
template: `
-
- Open Modal (Merged Header)
-
+
Open Modal (Merged Header)
Custom Header Area
@@ -185,7 +159,7 @@ export const MergedHeader: Story = {
export const NotClosable: Story = {
render: () => ({
- components: { NewModal, ButtonStyled },
+ components: { NewModal, Button },
setup() {
const modalRef = ref
| null>(null)
const openModal = () => modalRef.value?.show()
@@ -193,17 +167,13 @@ export const NotClosable: Story = {
},
template: `
-
- Open Non-Closable Modal
-
+
Open Non-Closable Modal
This modal cannot be closed by clicking outside or pressing escape.
Only the action button can close it.
-
- I understand, close
-
+ I understand, close
@@ -214,7 +184,7 @@ export const NotClosable: Story = {
export const NoPadding: Story = {
render: () => ({
- components: { NewModal, ButtonStyled },
+ components: { NewModal, Button },
setup() {
const modalRef = ref
| null>(null)
const openModal = () => modalRef.value?.show()
@@ -222,16 +192,12 @@ export const NoPadding: Story = {
},
template: `
-
- Open Modal (No Padding)
-
+
Open Modal (No Padding)
This modal has no default padding on the content area.
-
- Close
-
+ Close
diff --git a/packages/ui/src/stories/modal/ShareModal.stories.ts b/packages/ui/src/stories/modal/ShareModal.stories.ts
index 10b07701f4..28c81af2ae 100644
--- a/packages/ui/src/stories/modal/ShareModal.stories.ts
+++ b/packages/ui/src/stories/modal/ShareModal.stories.ts
@@ -1,7 +1,7 @@
import type { StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
+import { Button } from '../../components/base/buttons'
import ShareModal from '../../components/modal/ShareModal.vue'
const meta = {
@@ -20,7 +20,7 @@ export const LinkShare: Story = {
link: true,
},
render: (args) => ({
- components: { ShareModal, ButtonStyled },
+ components: { ShareModal, Button },
setup() {
const modalRef = ref
| null>(null)
const openModal = () => {
@@ -30,9 +30,7 @@ export const LinkShare: Story = {
},
template: `
-
- Open Link Share Modal
-
+ Open Link Share Modal
`,
@@ -47,7 +45,7 @@ export const TextShare: Story = {
link: false,
},
render: (args) => ({
- components: { ShareModal, ButtonStyled },
+ components: { ShareModal, Button },
setup() {
const modalRef = ref | null>(null)
const openModal = () => {
@@ -57,9 +55,7 @@ export const TextShare: Story = {
},
template: `
-
- Open Text Share Modal
-
+ Open Text Share Modal
`,
diff --git a/packages/ui/src/stories/modal/TabbedModal.stories.ts b/packages/ui/src/stories/modal/TabbedModal.stories.ts
index aee894f212..5d85e192ef 100644
--- a/packages/ui/src/stories/modal/TabbedModal.stories.ts
+++ b/packages/ui/src/stories/modal/TabbedModal.stories.ts
@@ -14,7 +14,7 @@ import {
import type { StoryObj } from '@storybook/vue3-vite'
import { defineComponent, h, ref } from 'vue'
-import ButtonStyled from '../../components/base/ButtonStyled.vue'
+import { Button } from '../../components/base/buttons'
import UnsavedChangesPopup from '../../components/base/UnsavedChangesPopup.vue'
import TabbedModal from '../../components/modal/TabbedModal.vue'
@@ -42,7 +42,7 @@ export default meta
export const Default: StoryObj = {
render: () => ({
- components: { TabbedModal, ButtonStyled },
+ components: { TabbedModal, Button },
setup() {
const modalRef = ref | null>(null)
const tabs = [
@@ -66,9 +66,7 @@ export const Default: StoryObj = {
},
template: /* html */ `
-
- Open Tabbed Modal
-
+ Open Tabbed Modal
`,
@@ -77,7 +75,7 @@ export const Default: StoryObj = {
export const WithTitleSlot: StoryObj = {
render: () => ({
- components: { TabbedModal, ButtonStyled, SettingsIcon },
+ components: { TabbedModal, SettingsIcon, Button },
setup() {
const modalRef = ref | null>(null)
const tabs = [
@@ -96,9 +94,7 @@ export const WithTitleSlot: StoryObj = {
},
template: /* html */ `
-
- Open with Title Slot
-
+ Open with Title Slot