From 8df5a0f0f9ced686077eb2cd1dd33fb2b9bbe9ad Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Wed, 7 Oct 2026 16:44:21 +0100 Subject: [PATCH 1/3] refactor: move sidebar out of app-shell --- apps/app-frontend/src/App.vue | 207 ++---------------- .../src/app/shell/sidebar/index.vue | 138 ++++++++++++ .../shell/sidebar/promotion.vue} | 48 ++-- .../MinecraftRequiredModal.vue | 8 +- .../campaigns/pride-fundraiser-banner.vue} | 14 +- .../friends/friends-list/index.vue} | 5 +- .../friends/friends-list/section.vue} | 0 .../friends}/use-friends.ts | 0 .../minecraft-accounts/accounts-card.vue} | 0 .../features/minecraft-accounts/context.ts | 16 ++ .../src/features/news/news-sidebar.vue | 51 +++++ .../app-frontend/src/features/news/queries.ts | 27 +++ .../onboarding/checklist.vue} | 0 apps/app-frontend/src/pages/Skins.vue | 33 ++- .../use-shared-instance-invite-candidates.ts | 2 +- apps/app-frontend/tailwind.config.ts | 1 + 16 files changed, 312 insertions(+), 238 deletions(-) create mode 100644 apps/app-frontend/src/app/shell/sidebar/index.vue rename apps/app-frontend/src/{components/ui/PromotionWrapper.vue => app/shell/sidebar/promotion.vue} (51%) rename apps/app-frontend/src/{components/ui/PrideFundraiserBanner.vue => features/campaigns/pride-fundraiser-banner.vue} (88%) rename apps/app-frontend/src/{components/ui/friends/FriendsList.vue => features/friends/friends-list/index.vue} (99%) rename apps/app-frontend/src/{components/ui/friends/FriendsSection.vue => features/friends/friends-list/section.vue} (100%) rename apps/app-frontend/src/{composables => features/friends}/use-friends.ts (100%) rename apps/app-frontend/src/{components/ui/AccountsCard.vue => features/minecraft-accounts/accounts-card.vue} (100%) create mode 100644 apps/app-frontend/src/features/minecraft-accounts/context.ts create mode 100644 apps/app-frontend/src/features/news/news-sidebar.vue create mode 100644 apps/app-frontend/src/features/news/queries.ts rename apps/app-frontend/src/{components/ui/onboarding-checklist/index.vue => features/onboarding/checklist.vue} (100%) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 48341d02f49..84a82a50ffa 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -7,7 +7,6 @@ import { ImageIcon, LogInIcon, LogOutIcon, - NewspaperIcon, PlayIcon, PlusIcon, RefreshCwIcon, @@ -24,7 +23,6 @@ import { AccountSwitchOverlay, Admonition, Avatar, - ButtonLink, commonMessages, commonSettingsMessages, ContentInstallModal, @@ -34,7 +32,6 @@ import { I18nDebugPanel, IconButton, LoadingBar, - NewsArticleCard, NotificationPanel, PopupNotificationPanel, provideLoadingState, @@ -65,10 +62,9 @@ import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch } from import { RouterView, useRoute, useRouter } from 'vue-router' import { useRouteLoading } from '@/app/runtime/use-route-loading' +import AppSidebar from '@/app/shell/sidebar/index.vue' import AppTitleBar from '@/app/shell/title-bar/index.vue' -import AccountsCard from '@/components/ui/AccountsCard.vue' import ErrorModal from '@/components/ui/ErrorModal.vue' -import FriendsList from '@/components/ui/friends/FriendsList.vue' import HostingUpdateRequired from '@/components/ui/HostingUpdateRequired.vue' import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue' import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue' @@ -81,9 +77,6 @@ import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyIn import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountRequiredModal.vue' import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue' import NavButton from '@/components/ui/NavButton.vue' -import OnboardingChecklist from '@/components/ui/onboarding-checklist/index.vue' -import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue' -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' @@ -101,6 +94,7 @@ import { useInstanceMetadataRefresh } from '@/composables/use-instance-metadata- import { useQuickInstanceLimit } from '@/composables/use-quick-instance-limit.ts' import { isDarkTheme, useTheme } from '@/composables/use-theme.ts' import { config } from '@/config' +import { provideMinecraftAccounts } from '@/features/minecraft-accounts/context' import { getAccountAppearance, rememberAccountAppearance } from '@/helpers/account-appearance.ts' import { hide_ads_window, @@ -229,7 +223,6 @@ async function handleFullscreenChange() { const APP_LEFT_NAV_WIDTH = '4rem' const APP_SIDEBAR_WIDTH = 300 const INTERCOM_BUBBLE_DEFAULT_PADDING = 20 -const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime() const credentials = ref() const storedModrinthAccounts = ref([]) let credentialsRefreshId = 0 @@ -254,9 +247,6 @@ const hostingUpdateRequired = computed( !!appUpdateState.availableUpdate.value && appUpdateState.updatesEnabled.value, ) -const prideFundraiserEnabled = computed( - () => appSettings.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE, -) const hostingIntercomIdentityKey = computed(() => { const rawServerId = route.params.id const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId @@ -373,8 +363,6 @@ const { (iconPath) => creationGeneratedIcon.value?.path === iconPath ? creationGeneratedIcon.value.config : null, ) -const { hasLoggedIntoMinecraft, hasLoggedIntoModrinth, showChecklist } = onboardingChecklist -const showFriendsList = computed(() => !showChecklist.value || hasLoggedIntoModrinth.value) async function randomizeCreationIcon() { const generated = await creationIconEditorModal.value?.randomizeAndSave() @@ -412,7 +400,6 @@ function onCreationIconSaved(iconPath, config) { context.instanceIconPath.value = iconPath } -const news = ref([]) const displayedServerInviteNotifications = new Set() const serverInvitePopupNotificationIds = new Set() let liveNotificationGeneration = 0 @@ -651,18 +638,6 @@ const messages = defineMessages({ 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) { @@ -854,22 +829,6 @@ async function setupApp() { ) }) - fetch(`https://modrinth.com/news/feed/articles.json`) - .then((response) => response.json()) - .then((res) => { - if (res && res.articles) { - news.value = res.articles - .map((article) => ({ - ...article, - path: article.link, - })) - .slice(0, 4) - } - }) - .catch((error) => { - console.error('Failed to fetch news articles', error) - }) - traceStartupStep('Read opening command', get_opening_command).then(handleCommand) traceStartupStep('Refresh startup credentials', fetchCredentials) @@ -907,13 +866,6 @@ const handleClose = async () => { await getCurrentWindow().close() } -const sidebarOverlayScrollbarsOptions = Object.freeze({ - overflow: { - x: 'hidden', - y: 'scroll', - }, -}) - const queryClient = useQueryClient() watch(stateInitialized, (ready) => { @@ -1442,7 +1394,7 @@ const modrinthAccountMenuOptions = computed(() => [ id: 'add-friend', label: formatMessage(messages.addFriend), icon: UserPlusIcon, - action: () => friendsList.value?.showAddFriendModal(), + action: () => sidebar.value?.showAddFriendModal(), }, { id: 'flags', @@ -1530,9 +1482,8 @@ onMounted(() => { setServerUpdateToPlayModal(updateToPlayModal.value) }) -const accounts = ref(null) -const friendsList = ref(null) -provide('accountsCard', accounts) +const sidebar = ref(null) +provideMinecraftAccounts(computed(() => sidebar.value?.minecraftAccounts ?? null)) useAppEvent('command', handleCommand, appEvents) useAppEvent('notification', handleLiveNotification, appEvents) @@ -2339,87 +2290,16 @@ provideAppUpdateDownloadProgress(appUpdateDownload) -
-
- - - -
- -
+ @@ -2536,41 +2416,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) overflow: hidden; } -.app-sidebar { - overflow: visible; - width: 300px; - position: relative; - height: calc(100vh - var(--top-bar-height)); - background: var(--brand-gradient-bg); - - --color-button-bg: var(--brand-gradient-button); - --surface-4: var(--brand-gradient-button); - --color-button-bg-hover: var(--brand-gradient-border); - --surface-5: var(--brand-gradient-border); - --color-divider: var(--brand-gradient-border); - --color-divider-dark: var(--brand-gradient-border); -} - -.app-sidebar::after { - content: ''; - position: absolute; - bottom: 250px; - left: 0; - right: 0; - height: 5rem; - background: var(--brand-gradient-fade-out-color); - pointer-events: none; -} - -.app-sidebar.has-plus::after { - display: none; -} - .disable-advanced-rendering { - .app-sidebar::before { - box-shadow: none; - } - &.app-contents::before { box-shadow: none; } @@ -2582,17 +2428,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload) } } -.app-sidebar::before { - content: ''; - box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.1) inset; - top: 0; - bottom: 0; - left: -2rem; - width: 2rem; - position: absolute; - pointer-events: none; -} - .app-viewport { flex-grow: 1; height: 100%; @@ -2617,18 +2452,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload) pointer-events: none; } -.sidebar-teleport-content { - display: contents; -} - -.sidebar-default-content { - display: none; -} - -.sidebar-teleport-content:empty + .sidebar-default-content.sidebar-enabled { - display: contents; -} - @media (prefers-reduced-motion: no-preference) { .nav-button-animated-enter-active { transition: all 0.5s cubic-bezier(0.15, 1.4, 0.64, 0.96); diff --git a/apps/app-frontend/src/app/shell/sidebar/index.vue b/apps/app-frontend/src/app/shell/sidebar/index.vue new file mode 100644 index 00000000000..29d46af312a --- /dev/null +++ b/apps/app-frontend/src/app/shell/sidebar/index.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/apps/app-frontend/src/components/ui/PromotionWrapper.vue b/apps/app-frontend/src/app/shell/sidebar/promotion.vue similarity index 51% rename from apps/app-frontend/src/components/ui/PromotionWrapper.vue rename to apps/app-frontend/src/app/shell/sidebar/promotion.vue index 16615831d58..b17a544b9dd 100644 --- a/apps/app-frontend/src/components/ui/PromotionWrapper.vue +++ b/apps/app-frontend/src/app/shell/sidebar/promotion.vue @@ -1,11 +1,20 @@ - - diff --git a/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue b/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue index 73293f5bed1..4437d405011 100644 --- a/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue +++ b/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue @@ -59,16 +59,16 @@ diff --git a/apps/app-frontend/src/components/ui/friends/FriendsList.vue b/apps/app-frontend/src/features/friends/friends-list/index.vue similarity index 99% rename from apps/app-frontend/src/components/ui/friends/FriendsList.vue rename to apps/app-frontend/src/features/friends/friends-list/index.vue index 08f0d43dd56..812cf7b5d0b 100644 --- a/apps/app-frontend/src/components/ui/friends/FriendsList.vue +++ b/apps/app-frontend/src/features/friends/friends-list/index.vue @@ -13,14 +13,15 @@ import { } from '@modrinth/ui' import { computed, ref } from 'vue' -import FriendsSection from '@/components/ui/friends/FriendsSection.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import { useAppSettings } from '@/composables/use-app-settings.ts' -import { useFriends } from '@/composables/use-friends' import type { FriendWithUserData } from '@/helpers/friends.ts' import type { ModrinthCredentials } from '@/helpers/mr_auth' import { get as getSettings, set as setSettings } from '@/helpers/settings.ts' +import { useFriends } from '../use-friends' +import FriendsSection from './section.vue' + const { formatMessage } = useVIntl() const { handleError } = injectNotificationManager() diff --git a/apps/app-frontend/src/components/ui/friends/FriendsSection.vue b/apps/app-frontend/src/features/friends/friends-list/section.vue similarity index 100% rename from apps/app-frontend/src/components/ui/friends/FriendsSection.vue rename to apps/app-frontend/src/features/friends/friends-list/section.vue diff --git a/apps/app-frontend/src/composables/use-friends.ts b/apps/app-frontend/src/features/friends/use-friends.ts similarity index 100% rename from apps/app-frontend/src/composables/use-friends.ts rename to apps/app-frontend/src/features/friends/use-friends.ts diff --git a/apps/app-frontend/src/components/ui/AccountsCard.vue b/apps/app-frontend/src/features/minecraft-accounts/accounts-card.vue similarity index 100% rename from apps/app-frontend/src/components/ui/AccountsCard.vue rename to apps/app-frontend/src/features/minecraft-accounts/accounts-card.vue diff --git a/apps/app-frontend/src/features/minecraft-accounts/context.ts b/apps/app-frontend/src/features/minecraft-accounts/context.ts new file mode 100644 index 00000000000..958097bfd74 --- /dev/null +++ b/apps/app-frontend/src/features/minecraft-accounts/context.ts @@ -0,0 +1,16 @@ +import { createContext } from '@modrinth/ui' +import type { Ref } from 'vue' + +import type { Skin } from '@/helpers/skins' + +export interface MinecraftAccountsActions { + readonly loginDisabled: boolean + refreshValues: () => Promise + setEquippedSkin: (skin: Skin) => Promise + setLoginDisabled: (disabled: boolean) => void + login: () => Promise +} + +export const [injectMinecraftAccounts, provideMinecraftAccounts] = createContext< + Readonly> +>('root', 'minecraftAccounts') diff --git a/apps/app-frontend/src/features/news/news-sidebar.vue b/apps/app-frontend/src/features/news/news-sidebar.vue new file mode 100644 index 00000000000..17b82dcbbf3 --- /dev/null +++ b/apps/app-frontend/src/features/news/news-sidebar.vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/app-frontend/src/features/news/queries.ts b/apps/app-frontend/src/features/news/queries.ts new file mode 100644 index 00000000000..b6ea3612a92 --- /dev/null +++ b/apps/app-frontend/src/features/news/queries.ts @@ -0,0 +1,27 @@ +import type { NewsArticle } from '@modrinth/ui' +import { queryOptions } from '@tanstack/vue-query' + +type NewsFeedArticle = Omit & { link: string } + +export const newsKeys = { + articles: ['news', 'articles'] as const, +} + +export function newsArticlesQueryOptions() { + return queryOptions({ + queryKey: newsKeys.articles, + queryFn: async ({ signal }): Promise => { + const response = await fetch('https://modrinth.com/news/feed/articles.json', { signal }) + if (!response.ok) { + throw new Error(`Failed to fetch news articles: HTTP ${response.status}`) + } + const feed: { articles?: NewsFeedArticle[] } | null = await response.json() + return (feed?.articles ?? []).map((article) => ({ ...article, path: article.link })) + }, + staleTime: Infinity, + retry: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }) +} diff --git a/apps/app-frontend/src/components/ui/onboarding-checklist/index.vue b/apps/app-frontend/src/features/onboarding/checklist.vue similarity index 100% rename from apps/app-frontend/src/components/ui/onboarding-checklist/index.vue rename to apps/app-frontend/src/features/onboarding/checklist.vue diff --git a/apps/app-frontend/src/pages/Skins.vue b/apps/app-frontend/src/pages/Skins.vue index ca84f5764f0..30ec659fc18 100644 --- a/apps/app-frontend/src/pages/Skins.vue +++ b/apps/app-frontend/src/pages/Skins.vue @@ -25,15 +25,14 @@ import { arrayBufferToBase64 } from '@modrinth/utils' import { useQuery } from '@tanstack/vue-query' import { type DragDropEvent, getCurrentWebview } from '@tauri-apps/api/webview' import { computedAsync } from '@vueuse/core' -import type { Ref } from 'vue' -import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue' +import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue' import EarsModIcon from '@/assets/skins/ears-mod.png' -import type AccountsCard from '@/components/ui/AccountsCard.vue' import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue' import VirtualSkinSectionList from '@/components/ui/skin/VirtualSkinSectionList.vue' import { useAppSettings } from '@/composables/use-app-settings.ts' import { handleSevereError } from '@/composables/use-error.js' +import { injectMinecraftAccounts } from '@/features/minecraft-accounts/context' import { trackEvent } from '@/helpers/analytics' import { check_reachable, get_default_user, login as login_flow, users } from '@/helpers/auth' import { cleanupUnusedPreviews } from '@/helpers/rendering/skin-previews' @@ -229,7 +228,7 @@ const skins = ref([]) const capes = ref([]) const offline = ref(!navigator.onLine) -const accountsCard = inject('accountsCard') as Ref +const minecraftAccounts = injectMinecraftAccounts() const currentUser = ref(undefined) const currentUserId = ref(undefined) @@ -578,7 +577,7 @@ function setLocallyEquippedSkin(skinToApply: Skin) { originalSelectedSkin.value = skins.value.find((skin) => skinsMatch(skin, skinToApply)) ?? skinToApply selectedSkin.value = originalSelectedSkin.value - void accountsCard.value?.setEquippedSkin(originalSelectedSkin.value) + void minecraftAccounts.value?.setEquippedSkin(originalSelectedSkin.value) } function insertLocalSkin(savedSkin: Skin) { @@ -634,7 +633,7 @@ function updateLocalSkin(savedSkin: Skin, applied: boolean, previousSkin?: Skin) originalSelectedSkin.value = locallyEquippedSkin selectedSkin.value = locallyEquippedSkin - void accountsCard.value?.setEquippedSkin(locallyEquippedSkin) + void minecraftAccounts.value?.setEquippedSkin(locallyEquippedSkin) } else { const locallySavedSkin = skins.value.find((skin) => skin.texture_key === savedSkin.texture_key) ?? savedSkin @@ -725,7 +724,7 @@ async function preserveExternalSkins(skinsToPersist: Skin[]) { if (skinsMatchIgnoringSource(originalSelectedSkin.value, skin)) { originalSelectedSkin.value = preservedSkin - void accountsCard.value?.setEquippedSkin(preservedSkin) + void minecraftAccounts.value?.setEquippedSkin(preservedSkin) } preservedSkins.push(preservedSkin) @@ -760,8 +759,8 @@ function schedulePendingSkinRefresh() { return } - if (accountsCard.value) { - await accountsCard.value.refreshValues() + if (minecraftAccounts.value) { + await minecraftAccounts.value.refreshValues() } await loadCapes() @@ -830,15 +829,15 @@ async function loadCurrentUser() { } async function login() { - accountsCard.value.setLoginDisabled(true) + minecraftAccounts.value?.setLoginDisabled(true) const loggedIn = await login_flow().catch(handleSevereError) - if (loggedIn && accountsCard) { - await accountsCard.value.refreshValues() + if (loggedIn && minecraftAccounts.value) { + await minecraftAccounts.value.refreshValues() } trackEvent('AccountLogIn') - accountsCard.value.setLoginDisabled(false) + minecraftAccounts.value?.setLoginDisabled(false) } function openAddSkinFileBrowser() { @@ -1061,7 +1060,7 @@ async function checkUserChanges() { try { const defaultId = await get_default_user() if (defaultId !== currentUserId.value) { - await accountsCard.value?.refreshValues() + await minecraftAccounts.value?.refreshValues() await loadCurrentUser() await loadCapes() await loadSkins() @@ -1295,13 +1294,13 @@ await loadSkins() diff --git a/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-candidates.ts b/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-candidates.ts index b25e9e91dfd..3d8830d42e8 100644 --- a/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-candidates.ts +++ b/apps/app-frontend/src/pages/instance/share/use-shared-instance-invite-candidates.ts @@ -5,7 +5,7 @@ import { } from '@modrinth/ui' import { computed, type Ref } from 'vue' -import { useFriends } from '@/composables/use-friends' +import { useFriends } from '@/features/friends/use-friends' import { getFriendUserId } from '@/helpers/friends.ts' import { get as getCredentials } from '@/helpers/mr_auth.ts' import { search_user } from '@/helpers/users.ts' diff --git a/apps/app-frontend/tailwind.config.ts b/apps/app-frontend/tailwind.config.ts index 301221bab4e..d4b216cd473 100644 --- a/apps/app-frontend/tailwind.config.ts +++ b/apps/app-frontend/tailwind.config.ts @@ -5,6 +5,7 @@ const config: Config = { content: [ './src/app/**/*.{js,vue,ts}', './src/components/**/*.{js,vue,ts}', + './src/features/**/*.{js,vue,ts}', './src/layouts/**/*.vue', './src/pages/**/*.vue', './src/plugins/**/*.{js,ts}', From 2cf9a42b422fc5012affd62460d56aae6aae8512 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Wed, 7 Oct 2026 16:50:03 +0100 Subject: [PATCH 2/3] feat: use scroll-indicator for sidebar --- .../src/app/shell/sidebar/index.vue | 118 +++++++++++------- .../src/app/shell/sidebar/promotion.vue | 10 ++ 2 files changed, 84 insertions(+), 44 deletions(-) diff --git a/apps/app-frontend/src/app/shell/sidebar/index.vue b/apps/app-frontend/src/app/shell/sidebar/index.vue index 29d46af312a..64223ab351e 100644 --- a/apps/app-frontend/src/app/shell/sidebar/index.vue +++ b/apps/app-frontend/src/app/shell/sidebar/index.vue @@ -1,6 +1,7 @@