1405/04/10-fix bug iphone show

This commit is contained in:
2026-07-01 12:49:43 +03:30
parent a133f6e11d
commit 40c8d21a0d
20 changed files with 315 additions and 71 deletions
+12
View File
@@ -112,6 +112,7 @@ import {
import { useDraggableFloatingButton } from '@/utils/useDraggableFloatingButton' import { useDraggableFloatingButton } from '@/utils/useDraggableFloatingButton'
import AppCreditLink from '@/components/AppCreditLink.vue' import AppCreditLink from '@/components/AppCreditLink.vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset'
const VersionUpdateModal = defineAsyncComponent( const VersionUpdateModal = defineAsyncComponent(
() => import('@/modules/version/VersionUpdateModal.vue') () => import('@/modules/version/VersionUpdateModal.vue')
@@ -251,6 +252,17 @@ watch(showSplash, (visible) => {
} }
}) })
watch(
() => route.name,
(toName, fromName) => {
if (isDesktop.value) return
if (fromName === 'login' || toName === 'home') {
recoverMobileViewportAfterAuth()
}
},
{ flush: 'post' },
)
</script> </script>
<style lang="less"> <style lang="less">
+2
View File
@@ -11,6 +11,7 @@ import { useUserStore } from '@/store/userStore'
import { getToken } from '@/utils' import { getToken } from '@/utils'
import '@/utils/useDateTime' import '@/utils/useDateTime'
import { initPriceRealtimeChannel } from '@/utils/priceSocket' import { initPriceRealtimeChannel } from '@/utils/priceSocket'
import { initMobileScrollRestoration } from '@/layouts/composables/useMobileScrollReset'
type BootWindow = Window & { type BootWindow = Window & {
__BOOT_TIMEOUT__?: number __BOOT_TIMEOUT__?: number
@@ -122,6 +123,7 @@ export async function bootstrapApp() {
} }
setupRouterGuards() setupRouterGuards()
initMobileScrollRestoration()
app.mount('#app') app.mount('#app')
+8 -1
View File
@@ -7,7 +7,7 @@ import { useFundUserStore } from '@/store/fundStore'
import type { NavigationGuardNext } from 'vue-router' import type { NavigationGuardNext } from 'vue-router'
import { useAuthStore } from '@/store/useAuthStore' import { useAuthStore } from '@/store/useAuthStore'
import { getConfigApp } from '@/utils/config' import { getConfigApp } from '@/utils/config'
import { getFingerPosition, setFingerPosition } from '@/app/authBoot' import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset'
const PUBLIC_ROUTE_NAMES = ['login', '404', 'educate', 'aboutUs'] as const const PUBLIC_ROUTE_NAMES = ['login', '404', 'educate', 'aboutUs'] as const
@@ -119,6 +119,13 @@ export function setupRouterGuards() {
} }
}) })
router.afterEach((to, from) => {
if (document.documentElement.classList.contains('app-layout--desktop')) return
if (from.name === 'login') {
recoverMobileViewportAfterAuth()
}
})
watch(currentRoute, async () => (userStore.userInfo ? '' : await start())) watch(currentRoute, async () => (userStore.userInfo ? '' : await start()))
} }
@@ -0,0 +1,125 @@
import { nextTick } from 'vue'
import router from '@/router'
let recoveryTimers: ReturnType<typeof setTimeout>[] = []
function clearRecoveryTimers() {
recoveryTimers.forEach(clearTimeout)
recoveryTimers = []
}
function blurFocusedField() {
const active = document.activeElement
if (active instanceof HTMLElement && active !== document.body) {
active.blur()
}
}
function clearBodyScrollLock() {
document.body.classList.remove('van-overflow-hidden')
document.body.style.removeProperty('position')
document.body.style.removeProperty('top')
document.body.style.removeProperty('left')
document.body.style.removeProperty('right')
document.body.style.removeProperty('width')
document.body.style.removeProperty('height')
document.body.style.removeProperty('min-height')
document.body.style.removeProperty('overflow')
document.body.style.removeProperty('transform')
document.documentElement.style.removeProperty('overflow')
document.documentElement.style.removeProperty('height')
document.documentElement.style.removeProperty('transform')
const appRoot = document.getElementById('app')
if (appRoot) {
appRoot.style.removeProperty('position')
appRoot.style.removeProperty('top')
appRoot.style.removeProperty('height')
appRoot.style.removeProperty('min-height')
appRoot.style.removeProperty('overflow')
appRoot.style.removeProperty('transform')
}
}
function resetAllScrollPositions() {
window.scrollTo(0, 0)
document.documentElement.scrollTop = 0
document.body.scrollTop = 0
document
.querySelectorAll('.main-with-bottom-chrome-inner, .login-page, .login-page-root')
.forEach((node) => {
if (node instanceof HTMLElement) {
node.scrollTop = 0
}
})
}
function kickViewportReflow() {
window.dispatchEvent(new Event('resize'))
if (window.visualViewport) {
window.scrollTo(0, 0)
}
}
function runViewportRecovery() {
clearBodyScrollLock()
resetAllScrollPositions()
kickViewportReflow()
}
function scheduleViewportRecovery() {
clearRecoveryTimers()
runViewportRecovery()
nextTick(() => {
requestAnimationFrame(() => {
runViewportRecovery()
for (const delay of [50, 150, 300, 500]) {
recoveryTimers.push(window.setTimeout(runViewportRecovery, delay))
}
})
})
}
/** Reset scroll offsets after navigation — fixes iOS safe-area drift. */
export function resetMobileAppScroll() {
runViewportRecovery()
nextTick(() => {
requestAnimationFrame(runViewportRecovery)
})
}
export async function dismissMobileKeyboard() {
blurFocusedField()
await new Promise((resolve) => window.setTimeout(resolve, 120))
}
export async function prepareMobileViewportForAppShell() {
blurFocusedField()
resetAllScrollPositions()
await dismissMobileKeyboard()
}
/** Full recovery after auth — runs after home shell is mounted. */
export function recoverMobileViewportAfterAuth() {
scheduleViewportRecovery()
}
export const resetMobileAppScrollAfterLogin = recoverMobileViewportAfterAuth
export async function completeLoginNavigation() {
await prepareMobileViewportForAppShell()
await router.push({ name: 'home' })
await router.isReady()
recoverMobileViewportAfterAuth()
}
export function initMobileScrollRestoration() {
if (typeof history !== 'undefined' && 'scrollRestoration' in history) {
history.scrollRestoration = 'manual'
}
}
+21 -5
View File
@@ -1,9 +1,9 @@
<template> <template>
<div> <div class="mobile-app-layout">
<div :class="['main-with-bottom-chrome', { 'main-with-bottom-chrome--no-nav': hideMenu }]"> <div :class="['main-with-bottom-chrome', { 'main-with-bottom-chrome--no-nav': hideMenu }]">
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route: activeRoute }">
<transition :name="route.meta.transition || 'fade'"> <transition :name="activeRoute.meta.transition || 'fade'">
<component :is="Component" class="main-with-bottom-chrome-inner" /> <component :is="Component" :key="activeRoute.fullPath" class="main-with-bottom-chrome-inner" />
</transition> </transition>
</router-view> </router-view>
</div> </div>
@@ -19,13 +19,16 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useConfigStore } from '@/store/configStore' import { useConfigStore } from '@/store/configStore'
import { getProjectMobileNavLogo, isBoursecuProject } from '@/utils/config' import { getProjectMobileNavLogo, isBoursecuProject } from '@/utils/config'
import { useAppNavigation } from '../composables/useAppNavigation' import { useAppNavigation } from '../composables/useAppNavigation'
import { recoverMobileViewportAfterAuth, resetMobileAppScroll } from '../composables/useMobileScrollReset'
import MobileBottomNav from './MobileBottomNav.vue' import MobileBottomNav from './MobileBottomNav.vue'
const config = useConfigStore() const config = useConfigStore()
const route = useRoute()
const isBoursecu = isBoursecuProject() const isBoursecu = isBoursecuProject()
const navLogo = computed(() => getProjectMobileNavLogo(config.config?.avatar)) const navLogo = computed(() => getProjectMobileNavLogo(config.config?.avatar))
@@ -34,6 +37,19 @@ const {
navItemClass, navItemClass,
navigateTo, navigateTo,
} = useAppNavigation() } = useAppNavigation()
watch(
() => route.fullPath,
() => {
resetMobileAppScroll()
},
{ flush: 'post' },
)
onMounted(() => {
resetMobileAppScroll()
recoverMobileViewportAfterAuth()
})
</script> </script>
<style lang="less" src="./mobile-layout.less"></style> <style lang="less" src="./mobile-layout.less"></style>
+14 -2
View File
@@ -131,8 +131,16 @@
opacity: 0; opacity: 0;
} }
.mobile-app-layout {
height: 100%;
max-height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.main-with-bottom-chrome { .main-with-bottom-chrome {
height: calc(100vh - var(--app-bottom-chrome-height, 108px)); height: calc(100dvh - var(--app-bottom-chrome-height, 108px));
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -148,7 +156,11 @@
overscroll-behavior-y: contain; overscroll-behavior-y: contain;
} }
.main-with-bottom-chrome-inner--contained {
overflow: hidden;
}
.main-with-bottom-chrome--no-nav, .main-with-bottom-chrome--no-nav,
.main-with-bottom-chrome--no-nav .main-with-bottom-chrome-inner { .main-with-bottom-chrome--no-nav .main-with-bottom-chrome-inner {
height: calc(100vh - var(--app-credit-footer-offset, 28px)); height: calc(100dvh - var(--app-credit-footer-offset, 28px));
} }
+27
View File
@@ -5,6 +5,33 @@
*/ */
html:not(.app-layout--desktop) { html:not(.app-layout--desktop) {
html,
body,
#app {
min-height: 0;
height: 100dvh;
max-height: 100dvh;
overflow: hidden;
}
body {
overscroll-behavior: none;
}
.desktopConfig,
.app-main {
min-height: 0;
height: 100dvh;
max-height: 100dvh;
overflow: hidden;
}
.app-main > * {
height: 100%;
max-height: 100%;
overflow: hidden;
}
/* Teleport to body — dock bar must span viewport, not shrink to content */ /* Teleport to body — dock bar must span viewport, not shrink to content */
.app-dock-add-bar, .app-dock-add-bar,
.wallet-card-slider { .wallet-card-slider {
+40 -7
View File
@@ -107,17 +107,54 @@
overflow: hidden; overflow: hidden;
} }
.mobile-info-page__header {
z-index: 1999;
}
/* Decorative page background — fixed to viewport, never scrolls on iOS */
.mobile-page-bg,
.mobile-info-page__bg { .mobile-info-page__bg {
pointer-events: none; pointer-events: none;
position: absolute; position: fixed;
inset: 0;
overflow: hidden;
z-index: 0; z-index: 0;
top: 0;
left: 0;
right: 0;
bottom: var(--app-bottom-chrome-height, 108px);
overflow: hidden;
max-width: 100vw;
box-sizing: border-box;
img { img {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0;
width: 100%; width: 100%;
height: auto;
}
}
.mobile-page-bg--no-nav,
.mobile-info-page__bg {
bottom: var(--app-credit-footer-offset, 28px);
}
.mobile-page-bg--cover {
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
html:not(.app-layout--desktop) {
@media screen and (min-width: 426px) and (max-width: 767.98px) {
.mobile-page-bg,
.mobile-info-page__bg {
width: 426px;
max-width: 426px;
left: 50%;
right: auto;
transform: translateX(-50%);
}
} }
} }
@@ -152,10 +189,6 @@
z-index: 1998; z-index: 1998;
} }
.mobile-info-page__header {
z-index: 1999;
}
.mobile-content-box { .mobile-content-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1,13 +1,13 @@
<template> <template>
<div> <div class="relative">
<MobilePageHeader title="آدرس ها" show-back /> <div class="mobile-page-bg" aria-hidden="true">
<div> <img :src="img" alt="" />
<img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />
</div> </div>
<MobilePageHeader title="آدرس ها" show-back />
<div <div
:class="loading ? 'h-screen' : ''" :class="loading ? 'h-screen' : ''"
class="px-3 mpage-content pb-2 overflow-y-scroll mpage-scroll" class="relative z-1 px-3 mpage-content pb-2 overflow-y-scroll mpage-scroll"
> >
<div <div
v-if="isBazretalaProject()" v-if="isBazretalaProject()"
+18 -1
View File
@@ -40,7 +40,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue' import { computed, nextTick, onActivated, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/store/userStore' import { useUserStore } from '@/store/userStore'
import router from '@/router' import router from '@/router'
import Icon from '@/components/BasicPacks/BasicIcon.vue' import Icon from '@/components/BasicPacks/BasicIcon.vue'
@@ -48,6 +49,7 @@ import { getProjectMobileNavLogo, isBoursecuProject } from '@/utils/config'
const headerRef = ref<HTMLElement | null>(null) const headerRef = ref<HTMLElement | null>(null)
const isHeaderScrolled = ref(false) const isHeaderScrolled = ref(false)
const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
const projectLogo = computed(() => getProjectMobileNavLogo()) const projectLogo = computed(() => getProjectMobileNavLogo())
@@ -158,6 +160,21 @@ onMounted(async () => {
requestAnimationFrame(() => setupScrollTracking()) requestAnimationFrame(() => setupScrollTracking())
}) })
onActivated(() => {
isHeaderScrolled.value = false
setupScrollTracking()
})
watch(
() => route.fullPath,
() => {
if (route.name === 'home') {
isHeaderScrolled.value = false
nextTick(() => setupScrollTracking())
}
},
)
onUnmounted(() => { onUnmounted(() => {
teardownScrollTracking() teardownScrollTracking()
}) })
+6
View File
@@ -25,6 +25,8 @@ import BalanceSheet from './components/BalanceSheet.vue'
import ShareModal from './components/ShareModal.vue' import ShareModal from './components/ShareModal.vue'
import HomePageShell from './shell/HomePageShell.vue' import HomePageShell from './shell/HomePageShell.vue'
import { useHomePage } from './composables/useHomePage' import { useHomePage } from './composables/useHomePage'
import { onMounted } from 'vue'
import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset'
const { const {
userStore, userStore,
@@ -37,6 +39,10 @@ const {
goldVal, goldVal,
logout, logout,
} = useHomePage() } = useHomePage()
onMounted(() => {
recoverMobileViewportAfterAuth()
})
</script> </script>
<style scoped> <style scoped>
+2 -9
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="products-page-root"> <div class="products-page-root main-with-bottom-chrome-inner--contained">
<ResponsiveViewPair> <ResponsiveViewPair>
<template #mobile> <template #mobile>
<MobileProductsView /> <MobileProductsView />
@@ -104,16 +104,9 @@ const {
<style scoped> <style scoped>
.products-page-root { .products-page-root {
min-height: 100%;
height: 100%; height: 100%;
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
}
</style>
<style lang="less">
.products-page-root.main-with-bottom-chrome-inner {
overflow: hidden;
} }
</style> </style>
@@ -1,6 +1,6 @@
<template> <template>
<div class="products-mobile-root"> <div class="products-mobile-root">
<div class="products-mobile-root__bg" aria-hidden="true"> <div class="mobile-page-bg" aria-hidden="true">
<img :src="img" alt="" /> <img :src="img" alt="" />
</div> </div>
<MobilePageHeader title="خرید و فروش" /> <MobilePageHeader title="خرید و فروش" />
@@ -86,20 +86,6 @@ const storeClosedMessage = computed(() => getStoreClosedMessage())
overflow: hidden; overflow: hidden;
} }
.products-mobile-root__bg {
pointer-events: none;
position: absolute;
inset: 0;
overflow: hidden;
z-index: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.products-mobile-root__chrome { .products-mobile-root__chrome {
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -1,6 +1,10 @@
<template> <template>
<div> <div class="relative">
<div class="absolute inset-0 bg-cover bg-center opacity-60 pointer-events-none"></div> <div
class="mobile-page-bg mobile-page-bg--cover opacity-60"
aria-hidden="true"
:style="{ backgroundImage: `url(${img})` }"
></div>
<MobilePageHeader title="تراکنش ها"> <MobilePageHeader title="تراکنش ها">
<template #trailing> <template #trailing>
<BasicIcon icon="vuesax-linear:filter" @click="showFilter = true" /> <BasicIcon icon="vuesax-linear:filter" @click="showFilter = true" />
@@ -8,7 +12,7 @@
</MobilePageHeader> </MobilePageHeader>
<!-- <div ref="el" :class="loading ? 'h-screen' : ''" class="px-3 mpage-content pb-10">--> <!-- <div ref="el" :class="loading ? 'h-screen' : ''" class="px-3 mpage-content pb-10">-->
<div ref="el" class="transactions-page-body px-3 pt-16 pb-24"> <div ref="el" class="transactions-page-body relative z-1 px-3 mpage-content pb-24">
<list-anim v-if="!loading"> <list-anim v-if="!loading">
<Item v-for="item in items" :key="item.id" :item="item" /> <Item v-for="item in items" :key="item.id" :item="item" />
<div :ref="bindMobileObserver" v-show="!loading" class="w-full opacity-0 relative"> <div :ref="bindMobileObserver" v-show="!loading" class="w-full opacity-0 relative">
@@ -52,6 +56,7 @@ import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDateP
import ListAnim from '@/components/ListAnim.vue' import ListAnim from '@/components/ListAnim.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue' import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
import { DotLottieVue } from '@lottiefiles/dotlottie-vue' import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
import img from '@/assets/img/user_ly1.svg'
import { useTransactionsPageContext } from './composables/useTransactionsPage' import { useTransactionsPageContext } from './composables/useTransactionsPage'
const el = ref<HTMLElement | null>(null) const el = ref<HTMLElement | null>(null)
+1 -15
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="mobile-user-view"> <div class="mobile-user-view">
<div class="mobile-user-view__bg" aria-hidden="true"> <div class="mobile-page-bg" aria-hidden="true">
<img :src="img" alt="" /> <img :src="img" alt="" />
</div> </div>
<MobilePageHeader title="اطلاعات حساب کاربری" show-back /> <MobilePageHeader title="اطلاعات حساب کاربری" show-back />
@@ -114,20 +114,6 @@ const {
overflow: hidden; overflow: hidden;
} }
.mobile-user-view__bg {
pointer-events: none;
position: absolute;
inset: 0;
overflow: hidden;
z-index: 0;
img {
position: absolute;
bottom: 0;
width: 100%;
}
}
.mobile-user-view__scroll { .mobile-user-view__scroll {
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -1,6 +1,10 @@
<template> <template>
<div> <div class="relative">
<div class="absolute inset-0 bg-cover bg-center opacity-60" :style="{ backgroundImage: `url(${img})` }"></div> <div
class="mobile-page-bg mobile-page-bg--cover opacity-60"
aria-hidden="true"
:style="{ backgroundImage: `url(${img})` }"
></div>
<MobilePageHeader title="کیف پول" /> <MobilePageHeader title="کیف پول" />
<div class="mpage-subheader wrap-fix flex justify-evenly"> <div class="mpage-subheader wrap-fix flex justify-evenly">
<div class="flex justify-evenly w-full px-2 h-11 items-center bg-gray-50 relative rounded"> <div class="flex justify-evenly w-full px-2 h-11 items-center bg-gray-50 relative rounded">
+2 -1
View File
@@ -377,6 +377,7 @@ import { useWalletStore } from '@/store/walletStore'
import { useSettingsStore } from '@/store/settingsStore' import { useSettingsStore } from '@/store/settingsStore'
import { syncPriceRealtimeChannel } from '@/utils/priceSocket' import { syncPriceRealtimeChannel } from '@/utils/priceSocket'
import { setTruth } from '@/app/authBoot' import { setTruth } from '@/app/authBoot'
import { completeLoginNavigation } from '@/layouts/composables/useMobileScrollReset'
import {useConfigStore} from "@/store/configStore"; import {useConfigStore} from "@/store/configStore";
import { import {
getConfigApp, getConfigApp,
@@ -488,7 +489,7 @@ async function login(accessToken: string | undefined) {
/* if (userStore.userInfo?.type === (URLUserType() === 'customer' ? 1 : 2)) {*/ /* if (userStore.userInfo?.type === (URLUserType() === 'customer' ? 1 : 2)) {*/
await walletStore.getMyWallet() await walletStore.getMyWallet()
setTruth() setTruth()
await router.push({ name: 'home' }) await completeLoginNavigation()
/* } else { /* } else {
removeToken() removeToken()
userStore.removeInformation() userStore.removeInformation()
+3 -2
View File
@@ -16,6 +16,7 @@ import { useUserStore } from '@/store/userStore'
import { useWalletStore } from '@/store/walletStore' import { useWalletStore } from '@/store/walletStore'
import { useSettingsStore } from '@/store/settingsStore' import { useSettingsStore } from '@/store/settingsStore'
import { setTruth } from '@/app/authBoot' import { setTruth } from '@/app/authBoot'
import { completeLoginNavigation } from '@/layouts/composables/useMobileScrollReset'
export function useLoginForm() { export function useLoginForm() {
const { time, onFinish, reset } = useCountdownTimer('00:02:00', 'mm:ss') const { time, onFinish, reset } = useCountdownTimer('00:02:00', 'mm:ss')
@@ -55,7 +56,7 @@ export function useLoginForm() {
const fingerPrint = await authenticateWithFingerprint() const fingerPrint = await authenticateWithFingerprint()
if (fingerPrint) { if (fingerPrint) {
setTruth() setTruth()
await router.push({ name: 'home' }) await completeLoginNavigation()
} else { } else {
message.error('خطا در اعتبارسنجی') message.error('خطا در اعتبارسنجی')
} }
@@ -75,7 +76,7 @@ export function useLoginForm() {
await userStore.getMyInfo() await userStore.getMyInfo()
await walletStore.getMyWallet() await walletStore.getMyWallet()
setTruth() setTruth()
await router.push({ name: 'home' }) await completeLoginNavigation()
} }
function getcode() { function getcode() {
+9 -2
View File
@@ -78,11 +78,18 @@ const isGaroosi = computed(() => config.config?.name === 'garoosi')
<style scoped> <style scoped>
.login-page-root { .login-page-root {
width: 100%; width: 100%;
min-height: 100vh; height: 100%;
max-height: 100dvh;
overflow: hidden;
} }
.login-page { .login-page {
min-height: calc(100vh - var(--app-credit-footer-offset, 28px)); height: 100%;
max-height: calc(100dvh - var(--app-credit-footer-offset, 28px));
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
} }
.login-page__content { .login-page__content {
+5 -1
View File
@@ -32,6 +32,10 @@ const isBoursecu = isBoursecuProject()
<style scoped> <style scoped>
.login-page { .login-page {
min-height: calc(100vh - var(--app-credit-footer-offset, 28px)); min-height: calc(100dvh - var(--app-credit-footer-offset, 28px));
max-height: calc(100dvh - var(--app-credit-footer-offset, 28px));
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
} }
</style> </style>