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 {