From f08ce82c3a6c01ba791b0cf95b62a5745874b35e Mon Sep 17 00:00:00 2001
From: O3H <owen77stubbs@gmail.com>
Date: Sun, 24 Nov 2024 05:36:38 +0000
Subject: [PATCH] Fixes/qol in SelectedGame view

- Remove redundant div, put row on viewport instead.
- Fixed tab labels wrapping so that resizing flows nicely.
- Translated the "No mods installed" text.
- Partially fixed some margin and width calc issues by using the app store.
- "Start Modded" button now disabled while no mods are installed.
---
 frontend/src/assets/styles/global.css         |   1 -
 frontend/src/components/general/Sidebar.vue   |   4 +-
 frontend/src/components/reusable/Viewport.vue |   9 +-
 .../selected-game/ProfileManager.vue          |   2 +-
 frontend/src/i18n/locales/de.json             |   3 +-
 frontend/src/i18n/locales/en.json             |   3 +-
 frontend/src/i18n/locales/es.json             |   3 +-
 frontend/src/i18n/locales/fr.json             |   3 +-
 frontend/src/i18n/locales/it.json             |   3 +-
 frontend/src/stores/index.ts                  |  20 +-
 frontend/src/views/Dashboard.vue              |   3 +-
 frontend/src/views/SelectedGame.vue           | 388 ++++++++++--------
 12 files changed, 243 insertions(+), 199 deletions(-)

diff --git a/frontend/src/assets/styles/global.css b/frontend/src/assets/styles/global.css
index 65455a4..f58288d 100644
--- a/frontend/src/assets/styles/global.css
+++ b/frontend/src/assets/styles/global.css
@@ -39,7 +39,6 @@ body {
     padding: 0;
     width: 100%;
     height: 100%;
-    overflow: hidden;
 }
 
 body::before {
diff --git a/frontend/src/components/general/Sidebar.vue b/frontend/src/components/general/Sidebar.vue
index 40c1734..9a8bb11 100644
--- a/frontend/src/components/general/Sidebar.vue
+++ b/frontend/src/components/general/Sidebar.vue
@@ -14,7 +14,7 @@ const appInfo = useDialog('app-info')
 const appStore = useAppStore()
 const { 
     sidebarExpanded, 
-    sidebarWidth 
+    sidebarWidthPx
 } = storeToRefs(appStore)
 
 const Dashboard = () => router.push('/')
@@ -145,7 +145,7 @@ const ModDevTools = () => router.push('/mod-dev-tools')
     align-items: center;
     position: fixed;
     z-index: 999;
-    width: v-bind(sidebarWidth);
+    width: v-bind(sidebarWidthPx);
 }
 
 .collapsed {
diff --git a/frontend/src/components/reusable/Viewport.vue b/frontend/src/components/reusable/Viewport.vue
index f393c4c..3a03505 100644
--- a/frontend/src/components/reusable/Viewport.vue
+++ b/frontend/src/components/reusable/Viewport.vue
@@ -4,10 +4,9 @@ import { useAppStore } from '@stores'
 
 const appStore = useAppStore()
 const { 
-    sidebarWidth 
+    sidebarOffsetPx,
+    topbarHeight
 } = storeToRefs(appStore)
-
-const topbarHeight = '30px'
 </script>
 
 <template>
@@ -19,7 +18,7 @@ const topbarHeight = '30px'
 <style scoped>
 .viewport {
     max-height: calc(100vh - v-bind(topbarHeight));
-    margin-left: calc(v-bind(sidebarWidth) + 20px);
-    margin-right: 20px;
+    margin-left: calc(v-bind(sidebarOffsetPx));
+    /* margin-right: 30px; */
 }
 </style>
\ No newline at end of file
diff --git a/frontend/src/components/selected-game/ProfileManager.vue b/frontend/src/components/selected-game/ProfileManager.vue
index 29ee15c..e48784a 100644
--- a/frontend/src/components/selected-game/ProfileManager.vue
+++ b/frontend/src/components/selected-game/ProfileManager.vue
@@ -168,7 +168,7 @@ onMounted(async () => {
 
 <style scoped>
 .profile-manager {
-    margin: 30px 10px 20px 10px;
+    margin: 30px 0px 20px 0px;
     display: flex;
     flex-direction: column;
     flex: 1 0 auto;
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index 91c5102..ea12edf 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -95,6 +95,7 @@
       "header": "Profilmanager",
       "search-placeholder": "Suchprofile",
       "new-profile": "Neues Profil"
-    }
+    },
+    "no-mods-installed": "Keine Mods installiert."
   }
 }
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index 17d3bd4..8330661 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -26,7 +26,8 @@
       "edit-button": "Edit"
     },
     "empty-results": "No mods match the search query",
-    "loading-mod-list": "Loading mod list"
+    "loading-mod-list": "Loading mod list",
+    "no-mods-installed": "No mods installed."
   },
   "settings": {
     "select-language": "Select language",
diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json
index b52728d..9e2aac5 100644
--- a/frontend/src/i18n/locales/es.json
+++ b/frontend/src/i18n/locales/es.json
@@ -95,6 +95,7 @@
       "header": "Administrador de perfiles",
       "search-placeholder": "Buscar perfiles",
       "new-profile": "Nuevo perfil"
-    }
+    },
+    "no-mods-installed": "No hay modificaciones instaladas."
   }
 }
diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json
index d545fb6..107d7c3 100644
--- a/frontend/src/i18n/locales/fr.json
+++ b/frontend/src/i18n/locales/fr.json
@@ -95,6 +95,7 @@
       "header": "Gestionnaire de profil",
       "search-placeholder": "Rechercher des profils",
       "new-profile": "Nouveau profil"
-    }
+    },
+    "no-mods-installed": "Aucun mod installé."
   }
 }
diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json
index b06fe7b..f598ec6 100644
--- a/frontend/src/i18n/locales/it.json
+++ b/frontend/src/i18n/locales/it.json
@@ -95,6 +95,7 @@
       "header": "Gestore del profilo",
       "search-placeholder": "Cerca profili",
       "new-profile": "Nuovo profilo"
-    }
+    },
+    "no-mods-installed": "Nessuna mod installata."
   }
 }
diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts
index a5e4806..71ea37c 100644
--- a/frontend/src/stores/index.ts
+++ b/frontend/src/stores/index.ts
@@ -7,12 +7,23 @@ import { computed, ref } from 'vue'
 //     sidebarWidth: string
 // }
 
+// TODO: Instead of hardcoding height/width refs, they should all be
+//       computed and point to their respective element in the DOM.
 export const useAppStore = defineStore('AppStore', () => {
     const maxThreads = ref(2)
 
+    const topbarHeight = ref(30)
     const sidebarExpanded = ref(false)
-    const sidebarWidth = computed(() => sidebarExpanded.value ? '180px' : '75px')
-  
+
+    const sidebarMargin = ref(20)
+    const sidebarMarginPx = computed(() => `${sidebarMargin.value}px`)
+
+    const sidebarWidth = computed(() => sidebarExpanded.value ? 180 : 75)
+    const sidebarWidthPx = computed(() => `${sidebarWidth.value}px`)
+
+    const sidebarOffset = computed(() => sidebarWidth.value + sidebarMargin.value)
+    const sidebarOffsetPx = computed(() => `${sidebarOffset.value}px`)
+
     function toggleSidebar() {
         sidebarExpanded.value = !sidebarExpanded.value
     }
@@ -27,8 +38,11 @@ export const useAppStore = defineStore('AppStore', () => {
 
     return { 
         maxThreads,
+        topbarHeight,
         sidebarExpanded,
-        sidebarWidth,
+        sidebarMargin, sidebarMarginPx,
+        sidebarWidth, sidebarWidthPx,
+        sidebarOffset, sidebarOffsetPx,
         toggleSidebar,
         setMaxThreads
     }
diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue
index 892b596..acbdbf7 100644
--- a/frontend/src/views/Dashboard.vue
+++ b/frontend/src/views/Dashboard.vue
@@ -135,7 +135,8 @@ async function getPackages() {
 }
 
 :deep(.p-tab) {
-    font-size: 24px;
+    font-size: 25px;
+    font-weight: 450;
 }
 </style>
 <!-- #endregion -->
\ No newline at end of file
diff --git a/frontend/src/views/SelectedGame.vue b/frontend/src/views/SelectedGame.vue
index f9bdbfe..d96d909 100644
--- a/frontend/src/views/SelectedGame.vue
+++ b/frontend/src/views/SelectedGame.vue
@@ -21,11 +21,21 @@ import {
 } from "@components"
 
 import type { Package, Nullable } from "@types"
-import { useProfileStore, useGameStore } from "@stores"
+import { 
+    useAppStore,
+    useProfileStore,
+    useGameStore 
+} from "@stores"
 
 import { debounce } from "../util"
 import { storeToRefs } from "pinia"
 
+const appStore = useAppStore()
+const { 
+    sidebarMarginPx,
+    sidebarOffsetPx
+} = storeToRefs(appStore)
+
 const profileStore = useProfileStore()
 const { selectedProfile } = storeToRefs(profileStore)
 
@@ -52,6 +62,16 @@ const currentPageMods = ref<Package[]>([])
 const installing = ref(false)
 const lastInstalledMod = ref<Nullable<v1.PackageVersion>>(null)
 
+const startModdedDisabled = () => {
+    const profileMods = selectedProfile.value?.mods
+    if (!profileMods) return true
+
+    const noTsMods = (profileMods?.thunderstore?.length || 0) < 1
+    const noNexusMods = (profileMods?.nexus?.length || 0) < 1
+
+    return noNexusMods && noTsMods
+}
+
 const activeTabIndex = ref(0)
 const tabs = ref([
     { label: 'This Profile', icon: 'pi pi-box' },
@@ -217,179 +237,179 @@ const handleScroll = (e: WheelEvent) => {
 
 <template>
 <Viewport :class="['selected-game', { 'no-drag': configEditorDialog.visible || installingModDialog.visible }]">
-    <div class="flex row">
-        <div class="flex column">
-            <Card class="selected-game-card no-drag">
-                <template #title>
-                    <p class="selected-game-card-header mt-0 mb-2">
-                        {{ $t('selected-game.currently-selected') }}
-                    </p>
-                </template>
-        
-                <template #content>
-                    <div class="flex no-drag">
-                        <div class="game-thumbnail-container">
-                            <img class="selected-game-thumbnail-background" :src="gameThumbnail()"/>
-                            <img class="selected-game-thumbnail-foreground" :src="gameThumbnail()"/>
-                        </div>
+    <div class="flex column">
+        <Card class="selected-game-card no-drag">
+            <template #title>
+                <p class="selected-game-card-header mt-0 mb-2">
+                    {{ $t('selected-game.currently-selected') }}
+                </p>
+            </template>
     
-                        <div class="flex column ml-3 no-drag">
-                            <p class="selected-game-title mt-0 mb-0">{{ selectedGame.title }}</p>
-                            <div class="flex column gap-2 mt-3">
-                                <Button plain class="btn justify-left" 
-                                    icon="pi pi-caret-right"
-                                    :label="$t('selected-game.start-modded-button')"
-                                    @click="startModded"
-                                />
-        
-                                <Button plain class="btn justify-left" severity="secondary"
-                                    icon="pi pi-caret-right"
-                                    :label="$t('selected-game.start-vanilla-button')"
-                                    @click="startVanilla"
-                                />
-        
-                                <Button plain class="btn justify-left mt-4"
-                                    icon="pi pi-file-edit"
-                                    :label="$t('selected-game.config-button')"
-                                    @click="configEditorDialog.setVisible(true)"
-                                />
-                            </div>
-                        </div>
+            <template #content>
+                <div class="flex no-drag">
+                    <div class="game-thumbnail-container">
+                        <img class="selected-game-thumbnail-background" :src="gameThumbnail()"/>
+                        <img class="selected-game-thumbnail-foreground" :src="gameThumbnail()"/>
                     </div>
-                </template>
-            </Card>
 
-            <ProfileManager @profileSelected="updatePage(0, ROWS)"/>
-        </div>
+                    <div class="flex column ml-3 flex-grow-1">
+                        <p class="selected-game-title mt-0 mb-0">{{ selectedGame.title }}</p>
+                        
+                        <div class="flex column gap-2 mt-3">
+                            <Button plain class="btn justify-left" 
+                                icon="pi pi-caret-right"
+                                :label="$t('selected-game.start-modded-button')"
+                                :disabled="startModdedDisabled"
+                                @click="startModded"
+                            />
     
-        <div class="flex mod-list-container">
-            <!-- Show skeleton of mod list while loading -->
-            <DataView v-if="loading" data-key="mod-list-loading" layout="list">
-                <template #empty>
-                    <div class="list-nogutter pt-4">
-                        <div v-for="i in 6" :key="i" class="loading-list-item">
-                            <div style="width: 1280px;" class="flex flex-row ml-1 p-3 border-top-faint border-round">
-                                <Skeleton size="6.5rem"/> <!-- Thumbnail -->
-                                
-                                <div class="flex column gap-1 ml-2">
-                                    <Skeleton height="1.5rem" width="20rem"/> <!-- Title -->
-                                    <Skeleton width="65rem"/> <!-- Description -->
-
-                                    <div class="flex row gap-2">
-                                        <Skeleton class="mt-3" width="6.8rem" height="2.2rem"/> <!-- Install Button -->
-                                        
-                                        <div class="flex row gap-1 align-items-center">
-                                            <Skeleton class="mt-3" width="2.8rem" height="2.2rem"/> <!-- Like button -->
-                                            <Skeleton class="mt-3" width="1.8rem" height="1.6rem"/> <!-- Likes -->
-                                        </div>
+                            <Button plain class="btn justify-left" severity="secondary"
+                                icon="pi pi-caret-right"
+                                :label="$t('selected-game.start-vanilla-button')"
+                                @click="startVanilla"
+                            />
+    
+                            <Button plain class="btn justify-left mt-4"
+                                icon="pi pi-file-edit"
+                                :label="$t('selected-game.config-button')"
+                                @click="configEditorDialog.setVisible(true)"
+                            />
+                        </div>
+                    </div>
+                </div>
+            </template>
+        </Card>
+
+        <ProfileManager @profileSelected="updatePage(0, ROWS)"/>
+    </div>
+
+    <div class="flex mod-list-container">
+        <!-- Show skeleton of mod list while loading -->
+        <DataView v-if="loading" data-key="mod-list-loading" layout="list">
+            <template #empty>
+                <div class="list-nogutter pt-4">
+                    <div v-for="i in 6" :key="i" class="loading-list-item">
+                        <div style="width: 1280px;" class="flex flex-row ml-1 p-3 border-top-faint border-round">
+                            <Skeleton size="6.5rem"/> <!-- Thumbnail -->
+                            
+                            <div class="flex column gap-1 ml-2">
+                                <Skeleton height="1.5rem" width="20rem"/> <!-- Title -->
+                                <Skeleton width="65rem"/> <!-- Description -->
+
+                                <div class="flex row gap-2">
+                                    <Skeleton class="mt-3" width="6.8rem" height="2.2rem"/> <!-- Install Button -->
+                                    
+                                    <div class="flex row gap-1 align-items-center">
+                                        <Skeleton class="mt-3" width="2.8rem" height="2.2rem"/> <!-- Like button -->
+                                        <Skeleton class="mt-3" width="1.8rem" height="1.6rem"/> <!-- Likes -->
                                     </div>
                                 </div>
                             </div>
                         </div>
                     </div>
-                </template>
-            </DataView>
-
-            <DataView
-                v-else lazy stripedRows
-                layout="list" data-key="mod-list"
-                paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink"
-                :paginator="mods.length > ROWS" :rows="ROWS"
-                :value="mods" @page="onPageChange" :first="first"
-            >
-                <template #empty>
-                    <div v-if="hasSearchInput()" class="pl-2">
-                        <h2 class="m-0 mt-1">{{ $t('selected-game.empty-results') }}.</h2>
-
-                        <!-- Sadge -->
-                        <img class="mt-2" src="https://cdn.7tv.app/emote/603cac391cd55c0014d989be/3x.png">
-                    </div>
-
-                    <!-- TODO: If failed, make this show regardless of search input. --> 
-                    <div v-else>
-                        <h2 v-if="activeTabIndex == 0" class="ml-1" style="color: orange; font-size: 24px; margin: 0;">
-                            No mods installed.
+                </div>
+            </template>
+        </DataView>
+
+        <DataView
+            v-else lazy stripedRows
+            layout="list" data-key="mod-list"
+            paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink"
+            :paginator="mods.length > ROWS" :rows="ROWS"
+            :value="mods" @page="onPageChange" :first="first"
+        >
+            <template #empty>
+                <div v-if="hasSearchInput()" class="pl-2">
+                    <h2 class="m-0 mt-1">{{ $t('selected-game.empty-results') }}.</h2>
+
+                    <!-- Sadge -->
+                    <img class="mt-2" src="https://cdn.7tv.app/emote/603cac391cd55c0014d989be/3x.png">
+                </div>
+
+                <!-- TODO: If failed, make this show regardless of search input. --> 
+                <div v-else>
+                    <h2 v-if="activeTabIndex == 0" class="empty-profile">
+                        {{ $t('selected-game.no-mods-installed') }}
+                    </h2>
+
+                    <div v-else class="ml-1">
+                        <h2 class="mb-2" style="color: red; font-size: 24px; margin: 0 auto;">
+                            No mods available! Something probably went wrong.
                         </h2>
 
-                        <div v-else class="ml-1">
-                            <h2 class="mb-2" style="color: red; font-size: 24px; margin: 0 auto;">
-                                No mods available! Something probably went wrong.
-                            </h2>
-
-                            <Button class="mt-1" :label="$t('keywords.refresh')" icon="pi pi-refresh" @click="refreshMods(true)"/>
-                        </div>
+                        <Button class="mt-1" :label="$t('keywords.refresh')" icon="pi pi-refresh" @click="refreshMods(true)"/>
                     </div>
-                </template>
-        
-                <template #header>
-                    <div class="flex row align-items-center gap-2">
-                        <div class="searchbar no-drag">
-                            <IconField iconPosition="left">
-                                <InputIcon class="pi pi-search"></InputIcon>
-                                <InputText type="text" :placeholder="$t('selected-game.search-mods')" 
-                                    v-model="searchInput" @input="onInputChange"
-                                />
-                            </IconField>
-                        </div>
-
-                        <TabMenu :model="tabs" @tab-change="onTabChange"/>
-                        <!-- <div class="flex row">
-                            <ModListDropdown>
-                                
-                            </ModListDropdown>
-                        </div> -->
+                </div>
+            </template>
+    
+            <template #header>
+                <div class="flex row align-items-center gap-2">
+                    <div class="searchbar no-drag">
+                        <IconField iconPosition="left">
+                            <InputIcon class="pi pi-search"></InputIcon>
+                            <InputText type="text" :placeholder="$t('selected-game.search-mods')" 
+                                v-model="searchInput" @input="onInputChange"
+                            />
+                        </IconField>
                     </div>
-                </template>
-        
-                <template #list>
-                    <div class="scrollable-list list-nogutter no-drag" @wheel.prevent="handleScroll">
-                        <div 
-                            v-for="(mod, index) in currentPageMods" class="list-item col-12"
-                            :key="index" :ref="el => modElements[index] = el"
-                        >
-                            <div class="flex-grow-1 flex column sm:flex-row align-items-center pt-2 gap-3" :class="{ 'border-top-faint': index != 0 }">
-                                <img class="mod-list-thumbnail block xl:block" :src="mod.latestVersion?.icon || ''"/>
-                                
-                                <div class="flex-grow-1 flex column md:flex-row md:align-items-center">
-                                    <div class="flex-grow-1 flex column justify-content-between">
-                                        <div class="flex row align-items-baseline">
-                                            <div class="mod-list-title">{{ mod.name }}</div>
-                                            <div class="mod-list-author">({{ mod.owner }})</div>
-                                        </div>
-
-                                        <div class="mod-list-description mb-1">{{ mod.latestVersion.description }}</div>
 
-                                        <!--
-                                            :icon="isFavouriteGame(game.identifier) ? 'pi pi-heart-fill' : 'pi pi-heart'"
-                                            :style="isFavouriteGame(game.identifier) ? { color: 'var(--primary-color)' } : {}"
-                                            @click="toggleFavouriteGame(game.identifier)"
-                                        /> -->
+                    <TabMenu :model="tabs" @tab-change="onTabChange"/>
+                    <!-- <div class="flex row">
+                        <ModListDropdown>
+                            
+                        </ModListDropdown>
+                    </div> -->
+                </div>
+            </template>
+    
+            <template #list>
+                <div class="scrollable-list list-nogutter no-drag" @wheel.prevent="handleScroll">
+                    <div 
+                        v-for="(mod, index) in currentPageMods" class="list-item col-12"
+                        :key="index" :ref="el => modElements[index] = el"
+                    >
+                        <div class="flex-grow-1 flex column sm:flex-row align-items-center pt-2 gap-3" :class="{ 'border-top-faint': index != 0 }">
+                            <img class="mod-list-thumbnail block xl:block" :src="mod.latestVersion?.icon || ''"/>
+                            
+                            <div class="flex-grow-1 flex column md:flex-row md:align-items-center">
+                                <div class="flex-grow-1 flex column justify-content-between">
+                                    <div class="flex row align-items-baseline">
+                                        <div class="mod-list-title">{{ mod.name }}</div>
+                                        <div class="mod-list-author">({{ mod.owner }})</div>
+                                    </div>
 
-                                        <div class="mod-list-bottom-row"> 
-                                            <div class="flex row gap-2">
-                                                <Button v-if="activeTabIndex == 0" class="btn w-full"
-                                                    severity="danger" icon="pi pi-trash"
-                                                    :label="$t('keywords.uninstall')"
-                                                />
-                                                <Button v-else class="btn w-full" icon="pi pi-download"
-                                                    :label="$t('keywords.install')" @click="installMod(mod.full_name)"
+                                    <div class="mod-list-description mb-1">{{ mod.latestVersion.description }}</div>
+
+                                    <!--
+                                        :icon="isFavouriteGame(game.identifier) ? 'pi pi-heart-fill' : 'pi pi-heart'"
+                                        :style="isFavouriteGame(game.identifier) ? { color: 'var(--primary-color)' } : {}"
+                                        @click="toggleFavouriteGame(game.identifier)"
+                                    /> -->
+
+                                    <div class="mod-list-bottom-row"> 
+                                        <div class="flex row gap-2">
+                                            <Button v-if="activeTabIndex == 0" class="btn w-full"
+                                                severity="danger" icon="pi pi-trash"
+                                                :label="$t('keywords.uninstall')"
+                                            />
+                                            <Button v-else class="btn w-full" icon="pi pi-download"
+                                                :label="$t('keywords.install')" @click="installMod(mod.full_name)"
+                                            />
+
+                                            <div class="flex row align-items-center">
+                                                <Button outlined plain 
+                                                    style="margin-right: 6.5px;"
+                                                    :icon="'pi pi-thumbs-up'"
                                                 />
-
-                                                <div class="flex row align-items-center">
-                                                    <Button outlined plain 
-                                                        style="margin-right: 6.5px;"
-                                                        :icon="'pi pi-thumbs-up'"
-                                                    />
-                                                    
-                                                    <div class="mod-list-rating">{{ mod.rating_score }}</div>
-                                                </div>
+                                                
+                                                <div class="mod-list-rating">{{ mod.rating_score }}</div>
                                             </div>
+                                        </div>
 
-                                            <!-- TODO: Ensure the tags flex to the end of the DataView and not the item content. -->
-                                            <div class="flex row flex-shrink-0 gap-1">
-                                                <div v-for="category in mod.categories.filter(c => c.toLowerCase() != 'mods')">
-                                                    <Tag :value="category"></Tag>
-                                                </div>
+                                        <!-- TODO: Ensure the tags flex to the end of the DataView and not the item content. -->
+                                        <div class="flex row flex-shrink-0 gap-1">
+                                            <div v-for="category in mod.categories.filter(c => c.toLowerCase() != 'mods')">
+                                                <Tag :value="category"></Tag>
                                             </div>
                                         </div>
                                     </div>
@@ -397,17 +417,16 @@ const handleScroll = (e: WheelEvent) => {
                             </div>
                         </div>
                     </div>
-                </template>
-            </DataView>
-
-        </div>
+                </div>
+            </template>
+        </DataView>
+    </div>
 
-        <ModInstallationOverlay :dialog="installingModDialog" 
-            :installing="installing" :lastInstalledMod="lastInstalledMod!"
-        />
+    <ModInstallationOverlay :dialog="installingModDialog" 
+        :installing="installing" :lastInstalledMod="lastInstalledMod!"
+    />
 
-        <ConfigEditorOverlay :dialog="configEditorDialog" :selectedGame="selectedGame"/>
-    </div>
+    <ConfigEditorOverlay :dialog="configEditorDialog" :selectedGame="selectedGame"/>
 </Viewport>
 </template>
 
@@ -419,8 +438,10 @@ const handleScroll = (e: WheelEvent) => {
 
 .selected-game {
     display: flex;
-    flex-direction: column;
+    flex-direction: row;
     margin-top: 30px;
+    /* Spacing between Card/ProfileManager and the DataView. */
+    gap: v-bind(sidebarMarginPx);
 }
 
 .selected-game > :first-child {
@@ -428,7 +449,7 @@ const handleScroll = (e: WheelEvent) => {
 }
 
 .selected-game-card {
-    margin: 0px 10px 0px 10px;
+    min-width: 380px;
     width: max-content;
     flex-shrink: 0;
     background: none;
@@ -453,18 +474,18 @@ const handleScroll = (e: WheelEvent) => {
 
 .selected-game-thumbnail-foreground {
     position: relative;
-    min-width: 160px;
+    min-width: 155px;
     max-width: 40%;
     max-height: 200px;
     border-radius: 3px;
     user-select: none;
-    border: 1px ridge rgba(255, 255, 255, 0.45);
+    border: 1px ridge rgba(255, 255, 255, 0.55);
 }
 
 .selected-game-thumbnail-background {
     position: absolute;
     z-index: -1;
-    filter: blur(6px);
+    filter: blur(4px);
     width: 100%;
     height: 100%;
 }
@@ -485,8 +506,7 @@ const handleScroll = (e: WheelEvent) => {
 }
 
 .mod-list-container {
-    max-width: 100vw;
-    width: 100vw;
+    max-width: 100%;
 }
 
 .mod-list-thumbnail {
@@ -532,22 +552,29 @@ const handleScroll = (e: WheelEvent) => {
     overflow-y: scroll;
     scrollbar-width: none;
     max-height: calc(100vh - 150px);
+    /* TODO: Replace magic number by getting it dynamically from selected game card width. */
+    max-width: calc(100vw - v-bind(sidebarOffsetPx) - 420px);
 }
 
-/*:deep(.p-dataview-layout-options .p-button) {
-    background: none !important;
-    border: none;
-}*/
+.empty-profile {
+    color: rgba(235, 235, 235, 0.95);
+    font-size: 25px;
+    margin: 0;
+}
 
 :deep(.p-tabmenu-tablist) {
     background: none !important;
 }
 
+:deep(.p-tabmenu-item-label) {
+    text-wrap: nowrap;
+}
+
 /* TODO: Investigate why this padding affects profile manager. */
 :deep(.p-dataview-header) {
     background: none !important;
-    padding: 10px 0px 10px 0px;
-    margin: 0px 5px 0px 5px;
+    padding: 0px 0px 10px 0px;
+    margin: 0;
     border: none;
 }
 
@@ -576,9 +603,8 @@ const handleScroll = (e: WheelEvent) => {
 
 .list-item {
     display: flex;
-    width: 1300px; /* TODO: Make this calc from right edge minus 30px. 100vw and 100% dont work? */
-    padding-bottom: 15px;
-    padding-top: 0px;
+    width: 100%;
+    padding: 0px 0px 10px 0px;
 }
 
 .loading-list-item {