Split desktop and mobile layouts with shared page data so APIs fetch once per context.

Add layout shells, desktop views, module-level page caches, and UI fixes for auth, home, wallet, transactions, address, and related pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-14 10:08:46 +03:30
co-authored by Cursor
parent 4404b4cb92
commit a5e458ed25
234 changed files with 18425 additions and 5503 deletions
+12
View File
@@ -0,0 +1,12 @@
<template>
<DesktopAppLayout v-if="isDesktopLayout" />
<MobileAppLayout v-else />
</template>
<script lang="ts" setup>
import { useDesktopBreakpoint } from './composables/useAppLayout'
import DesktopAppLayout from './desktop/DesktopAppLayout.vue'
import MobileAppLayout from './mobile/MobileAppLayout.vue'
const isDesktopLayout = useDesktopBreakpoint()
</script>
@@ -0,0 +1,47 @@
<template>
<div class="responsive-view-pair">
<div
v-show="!isDesktop"
class="responsive-view-pair__pane"
:class="{ 'responsive-pane--hidden': isDesktop }"
>
<slot name="mobile" />
</div>
<div
v-show="isDesktop"
class="responsive-view-pair__pane"
:class="{ 'responsive-pane--hidden': !isDesktop }"
>
<slot name="desktop" />
</div>
</div>
</template>
<script lang="ts" setup>
import { useDesktopBreakpoint } from '../composables/useResponsiveLayout'
const isDesktop = useDesktopBreakpoint()
</script>
<style scoped>
.responsive-view-pair {
min-height: 100%;
height: 100%;
width: 100%;
max-width: 100%;
min-width: 0;
overflow-x: hidden;
}
.responsive-view-pair__pane {
min-height: 100%;
height: 100%;
width: 100%;
max-width: 100%;
min-width: 0;
}
.responsive-pane--hidden {
/* v-show already sets display:none; class reserved for layout overrides */
}
</style>
+34
View File
@@ -0,0 +1,34 @@
import { useMediaQuery } from '@vueuse/core'
import { onUnmounted, watch } from 'vue'
import { DESKTOP_LAYOUT_MIN_WIDTH } from '../constants'
const DESKTOP_LAYOUT_CLASS = 'app-layout--desktop'
export function useDesktopBreakpoint() {
return useMediaQuery(`(min-width: ${DESKTOP_LAYOUT_MIN_WIDTH}px)`)
}
/** Apply html class for global desktop shell styles. Call once from App.vue. */
export function useApplyDesktopLayoutClass() {
const isDesktopLayout = useDesktopBreakpoint()
const stop = watch(
isDesktopLayout,
(desktop) => {
document.documentElement.classList.toggle(DESKTOP_LAYOUT_CLASS, desktop)
},
{ immediate: true },
)
onUnmounted(() => {
stop()
document.documentElement.classList.remove(DESKTOP_LAYOUT_CLASS)
})
}
/** @deprecated use useDesktopBreakpoint + useApplyDesktopLayoutClass */
export function useAppLayout() {
const isDesktopLayout = useDesktopBreakpoint()
useApplyDesktopLayoutClass()
return { isDesktopLayout }
}
@@ -0,0 +1,43 @@
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export function useAppNavigation() {
const router = useRouter()
const route = useRoute()
const popVisible = ref(false)
const currentRoute = computed(() => route.name)
const hideMenu = computed(() => !!route.meta.hideMenu)
function isNavActive(routeName: string) {
return currentRoute.value === routeName
}
function navItemClass(routeName: string) {
return isNavActive(routeName) ? 'text-primary' : 'text-gray-400'
}
function navigateTo(routeName: string) {
popVisible.value = false
router.push({ name: routeName })
}
function toggleOverflowMenu() {
popVisible.value = !popVisible.value
}
function closeOverflowMenu() {
popVisible.value = false
}
return {
popVisible,
currentRoute,
hideMenu,
isNavActive,
navItemClass,
navigateTo,
toggleOverflowMenu,
closeOverflowMenu,
}
}
@@ -0,0 +1,21 @@
import { useMediaQuery } from '@vueuse/core'
import { DESKTOP_LAYOUT_MIN_WIDTH } from '../constants'
/**
* Reactive desktop breakpoint. Use only for UI switching — never tie API calls to this ref.
*/
export function useDesktopBreakpoint() {
return useMediaQuery(`(min-width: ${DESKTOP_LAYOUT_MIN_WIDTH}px)`)
}
/**
* Guidelines for responsive pages (Vue):
*
* 1. Call page composables (with onMounted fetch) exactly once in the route `index.vue` shell.
* 2. Switch Mobile/Desktop views with `v-show`, not `v-if`, so resize does not destroy children.
* 3. Keep `<router-view>` outside responsive chrome wrappers so child routes are not remounted.
* 4. Mobile/Desktop views consume shared state via provide/inject — they must not fetch independently.
* 5. Prefer store/composable request dedup (in-flight promise + cached result) for shared data.
*/
export const RESPONSIVE_HIDDEN_CLASS = 'responsive-pane--hidden'
+29
View File
@@ -0,0 +1,29 @@
import { isBoursecuProject } from '@/utils/config'
import type { AppNavItem } from '../types'
const BOURSECU_PRIMARY_NAV: AppNavItem[] = [
{ id: 'buyProduct', routeName: 'buyProduct', label: 'سفارش‌ها', icon: 'vuesax-linear:box' },
{ id: 'transactions', routeName: 'transactions', label: 'تراکنش‌ها', icon: 'vuesax-linear:receipt-item' },
{ id: 'home', routeName: 'home', label: 'صفحه اصلی', icon: 'vuesax-linear:home' },
{ id: 'wallet', routeName: 'wallet', label: 'کیف پول', icon: 'vuesax-linear:wallet-1' },
{ id: 'user', routeName: 'user', label: 'تنظیمات', icon: 'vuesax-linear:setting-2' },
]
const DEFAULT_PRIMARY_NAV: AppNavItem[] = [
{ id: 'home', routeName: 'home', label: 'صفحه اصلی', icon: 'vuesax-linear:home' },
{ id: 'transactions', routeName: 'transactions', label: 'گزارش', icon: 'vuesax-linear:receipt-item' },
{ id: 'buyProduct', routeName: 'buyProduct', label: 'محصولات', icon: 'vuesax-linear:coin' },
{ id: 'wallet', routeName: 'wallet', label: 'کیف پول', icon: 'vuesax-linear:wallet-1' },
{ id: 'user', routeName: 'user', label: 'تنظیمات', icon: 'vuesax-linear:setting-2' },
]
export const SECONDARY_NAV: AppNavItem[] = [
{ id: 'notifyTrigger', routeName: 'notifyTrigger', label: 'هشدار قیمت', icon: 'vuesax-linear:bill' },
{ id: 'about', routeName: 'about', label: 'درباره ما', icon: 'vuesax-linear:info-circle' },
{ id: 'rules', routeName: 'rules', label: 'قوانین و مقررات', icon: 'vuesax-linear:task' },
{ id: 'giftCard', routeName: 'giftCard', label: 'ثبت کارت هدیه', icon: 'vuesax-linear:gift' },
]
export function getPrimaryNavItems(): AppNavItem[] {
return isBoursecuProject() ? BOURSECU_PRIMARY_NAV : DEFAULT_PRIMARY_NAV
}
+4
View File
@@ -0,0 +1,4 @@
/** Minimum viewport width for desktop app shell (sidebar layout). */
export const DESKTOP_LAYOUT_MIN_WIDTH = 768
export const DESKTOP_SIDEBAR_WIDTH = 260
+40
View File
@@ -0,0 +1,40 @@
<template>
<div class="desktop-app-layout">
<DesktopSidebar v-if="!hideMenu" />
<main
class="desktop-app-layout__main"
:class="{ 'desktop-app-layout__main--full': hideMenu }"
>
<router-view v-slot="{ Component, route }">
<component
:is="Component"
:key="route.fullPath"
class="desktop-app-layout__content"
/>
</router-view>
</main>
</div>
</template>
<script lang="ts" setup>
import { onUnmounted, watch } from 'vue'
import { useAppNavigation } from '../composables/useAppNavigation'
import DesktopSidebar from './DesktopSidebar.vue'
const NO_SIDEBAR_CLASS = 'app-layout--desktop-no-sidebar'
const { hideMenu } = useAppNavigation()
watch(
hideMenu,
(hidden) => {
document.documentElement.classList.toggle(NO_SIDEBAR_CLASS, hidden)
},
{ immediate: true },
)
onUnmounted(() => {
document.documentElement.classList.remove(NO_SIDEBAR_CLASS)
})
</script>
<style lang="less" src="./desktop-layout.less"></style>
+25
View File
@@ -0,0 +1,25 @@
<template>
<button
type="button"
class="desktop-nav-item"
:class="{ 'desktop-nav-item--active': active }"
@click="$emit('click')"
>
<BasicIcon :icon="icon" size="20" class="desktop-nav-item__icon" />
<span class="desktop-nav-item__label">{{ label }}</span>
</button>
</template>
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
defineProps<{
icon: string
label: string
active: boolean
}>()
defineEmits<{
click: []
}>()
</script>
+55
View File
@@ -0,0 +1,55 @@
<template>
<aside
class="desktop-sidebar"
:class="{ 'desktop-sidebar--boursecu': isBoursecu }"
>
<div class="desktop-sidebar__brand">
<img
:src="navLogo"
alt=""
class="desktop-sidebar__logo"
/>
</div>
<nav class="desktop-sidebar__section" aria-label="منوی اصلی">
<DesktopNavItem
v-for="item in primaryItems"
:key="item.id"
:icon="item.icon"
:label="item.label"
:active="isNavActive(item.routeName)"
@click="navigateTo(item.routeName)"
/>
</nav>
<div class="desktop-sidebar__divider" />
<nav class="desktop-sidebar__section desktop-sidebar__section--secondary" aria-label="منوی فرعی">
<DesktopNavItem
v-for="item in secondaryItems"
:key="item.id"
:icon="item.icon"
:label="item.label"
:active="isNavActive(item.routeName)"
@click="navigateTo(item.routeName)"
/>
</nav>
</aside>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useConfigStore } from '@/store/configStore'
import { getProjectNavLogo, isBoursecuProject } from '@/utils/config'
import { getPrimaryNavItems, SECONDARY_NAV } from '../config/navigation'
import { useAppNavigation } from '../composables/useAppNavigation'
import DesktopNavItem from './DesktopNavItem.vue'
const config = useConfigStore()
const isBoursecu = isBoursecuProject()
const navLogo = computed(() => getProjectNavLogo(config.config?.avatar))
const primaryItems = getPrimaryNavItems()
const secondaryItems = SECONDARY_NAV
const { isNavActive, navigateTo } = useAppNavigation()
</script>
+157
View File
@@ -0,0 +1,157 @@
.desktop-app-layout {
display: flex;
flex-direction: row;
min-height: 100vh;
width: 100%;
max-width: 100vw;
background: @prefixCls-background-color;
}
.desktop-app-layout__main {
flex: 1;
min-width: 0;
height: 100vh;
overflow: hidden;
}
.desktop-app-layout__main--full {
width: 100%;
}
.desktop-app-layout__content {
height: 100vh;
width: 100%;
min-height: 100vh;
overflow-y: auto;
overflow-x: hidden;
background: @prefixCls-background-color;
}
.desktop-sidebar {
flex-shrink: 0;
width: var(--app-desktop-sidebar-width, 260px);
height: 100vh;
display: flex;
flex-direction: column;
background: #fff;
position: sticky;
top: 0;
z-index: 100;
padding: 20px 12px 16px;
box-sizing: border-box;
}
.desktop-sidebar--boursecu {
background: var(--App-brand-nav, #133050);
.desktop-sidebar__divider {
background: rgba(255, 255, 255, 0.12);
}
.desktop-nav-item {
color: rgba(255, 255, 255, 0.72);
&:hover {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
&--active {
background: rgba(255, 255, 255, 0.14);
color: #fff;
.desktop-nav-item__icon {
color: #fff;
}
}
}
.desktop-nav-item__icon {
color: rgba(255, 255, 255, 0.85);
}
.desktop-sidebar__logo {
max-width: 200px;
max-height: 80px;
}
}
.desktop-sidebar__brand {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 12px 20px;
margin-bottom: 4px;
}
.desktop-sidebar__logo {
max-width: 140px;
max-height: 48px;
width: auto;
height: auto;
object-fit: contain;
}
.desktop-sidebar__section {
display: flex;
flex-direction: column;
gap: 4px;
}
.desktop-sidebar__section--secondary {
margin-top: auto;
}
.desktop-sidebar__divider {
height: 1px;
background: rgba(0, 0, 0, 0.08);
margin: 12px 8px;
}
.desktop-nav-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 12px;
border: none;
border-radius: 10px;
background: transparent;
cursor: pointer;
text-align: right;
font-family: inherit;
font-size: 14px;
font-weight: 600;
color: #6b7280;
transition: background 0.15s ease, color 0.15s ease;
&:hover {
background: rgba(0, 0, 0, 0.04);
color: @App-primary-color;
}
&--active {
background: rgba(0, 0, 0, 0.05);
color: @App-primary-color;
.desktop-nav-item__icon {
color: @App-primary-color;
}
}
}
.desktop-nav-item__icon {
flex-shrink: 0;
color: #9ca3af;
}
.desktop-nav-item__label {
flex: 1;
line-height: 1.3;
}
.desktop-sidebar__section--secondary .desktop-nav-item {
font-size: 13px;
font-weight: 500;
padding: 8px 12px;
}
@@ -0,0 +1,83 @@
/* Shared desktop page primitives */
.dpage-topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 28px;
padding-bottom: 24px;
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
&__back {
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
color: #6b7280;
font-size: 13px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
padding: 8px 0;
&:hover {
color: #111827;
}
}
&__meta {
flex: 1;
min-width: 0;
}
&__eyebrow {
font-size: 12px;
font-weight: 500;
color: #9ca3af;
margin: 0 0 4px;
}
&__title {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.03em;
color: #111827;
margin: 0;
line-height: 1.2;
}
&__subtitle {
font-size: 14px;
color: #6b7280;
margin: 6px 0 0;
line-height: 1.6;
}
}
.dpage-surface {
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 16px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.dpage-support {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
> a:first-child {
flex: 1;
min-width: 0;
}
&__bale {
width: 40px;
height: 40px;
object-fit: contain;
flex-shrink: 0;
}
}
+57
View File
@@ -0,0 +1,57 @@
<template>
<div>
<MobileOverflowMenu
:visible="popVisible"
:overflow-items="overflowItems"
@close="closeOverflowMenu"
@navigate="navigateTo"
/>
<div :class="['main-with-bottom-chrome', { 'main-with-bottom-chrome--no-nav': hideMenu }]">
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'">
<component :is="Component" class="main-with-bottom-chrome-inner" />
</transition>
</router-view>
</div>
<MobileBottomNav
v-if="!hideMenu"
:is-boursecu="isBoursecu"
:nav-logo="navLogo"
:nav-item-class="navItemClass"
@navigate="navigateTo"
@toggle-overflow="toggleOverflowMenu"
/>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useConfigStore } from '@/store/configStore'
import { getProjectNavLogo, isBoursecuProject } from '@/utils/config'
import { SECONDARY_NAV } from '../config/navigation'
import { useAppNavigation } from '../composables/useAppNavigation'
import MobileBottomNav from './MobileBottomNav.vue'
import MobileOverflowMenu from './MobileOverflowMenu.vue'
const config = useConfigStore()
const isBoursecu = isBoursecuProject()
const navLogo = computed(() => getProjectNavLogo(config.config?.avatar))
const {
popVisible,
hideMenu,
navItemClass,
navigateTo,
toggleOverflowMenu,
closeOverflowMenu,
} = useAppNavigation()
const overflowItems = [
{ id: 'user', routeName: 'user', label: 'حساب کاربری', icon: 'vuesax-linear:user' },
...SECONDARY_NAV,
]
</script>
<style lang="less" src="./mobile-layout.less"></style>
+107
View File
@@ -0,0 +1,107 @@
<template>
<div class="bottom-appbar w-auto mx-auto bg-white">
<div class="tabs bottom-0">
<div
v-if="isBoursecu"
:class="navItemClass('buyProduct')"
class="tab"
@click="$emit('navigate', 'buyProduct')"
>
<Icon icon="vuesax-linear:box" />
<div class="text-xs mt-1 font-bold">سفارش ها</div>
</div>
<div
v-else
:class="navItemClass('home')"
class="tab"
@click="$emit('navigate', 'home')"
>
<Icon icon="vuesax-linear:home" />
<div class="text-xs mt-1 font-bold">صفحه اصلی</div>
</div>
<div
:class="navItemClass('transactions')"
class="tab"
@click="$emit('navigate', 'transactions')"
>
<Icon icon="vuesax-linear:receipt-item" />
<div class="text-xs mt-1 font-bold">{{ isBoursecu ? 'تراکنش ها' : 'گزارش' }}</div>
</div>
<div
v-if="isBoursecu"
:class="navItemClass('home')"
class="tab"
@click="$emit('navigate', 'home')"
>
<Icon icon="vuesax-linear:home" />
<div class="text-xs mt-1 font-bold">صفحه اصلی</div>
</div>
<div v-else class="main_in" @click="$emit('navigate', 'buyProduct')">
<div
class="circle_in"
:class="isWhiteNavLogo ? 'is-white-nav-logo' : 'text-white'"
>
<div class="circle_into" @click.stop="$emit('toggle-overflow')">
<div
v-if="projectName === 'garoosi'"
class="p-2 text-tertiary flex flex-col justify-center items-center"
>
<Icon icon="vuesax-linear:coin" />
<div class="text-[9px] font-bold">محصولات</div>
</div>
<img
v-else
class="p-2"
:src="navLogo"
alt=""
/>
</div>
</div>
</div>
<div
:class="navItemClass('wallet')"
class="tab"
@click="$emit('navigate', 'wallet')"
>
<Icon icon="vuesax-linear:wallet-1" />
<div class="text-xs mt-1 font-bold">کیف پول</div>
</div>
<div
:class="navItemClass('user')"
class="tab"
@click="$emit('navigate', 'user')"
>
<Icon icon="vuesax-linear:setting-2" />
<div class="text-xs mt-1 font-bold">تنظیمات</div>
</div>
</div>
<div v-if="!isBoursecu" class="main">
<div class="circle" />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { getConfigApp } from '@/utils/config'
defineProps<{
isBoursecu: boolean
navLogo: string
navItemClass: (routeName: string) => string
}>()
defineEmits<{
navigate: [routeName: string]
'toggle-overflow': []
}>()
const projectName = getConfigApp('name')
const whiteNavLogoProjects = ['goldenramin']
const isWhiteNavLogo = computed(() => whiteNavLogoProjects.includes(projectName))
</script>
+53
View File
@@ -0,0 +1,53 @@
<template>
<Teleport to="body">
<Transition>
<div
v-if="visible"
class="top-0 bottom-0 bg-primary wrap-fix z-2000 opacity-60"
@click="$emit('close')"
/>
</Transition>
<Transition name="slide-fade">
<div v-if="visible" class="wrap-fix bottom-11vh z-2000">
<div class="relative flex flex-col items-center">
<div
class="flex items-center justify-center bg-white rounded-full w-[30px] h-[30px] mb-2"
@click="$emit('close')"
>
<BasicIcon icon="vuesax-linear:close-circle" />
</div>
<div class="flex flex-col items-center gap-2">
<BasicButton
v-for="item in overflowItems"
:key="item.id"
icon-size="16"
class="!font-bold w-15rem"
@click="$emit('navigate', item.routeName)"
>
<div class="flex gap-2 items-center">
<BasicIcon :icon="item.icon" size="16" class="absolute right-2" />
<div>{{ item.label }}</div>
</div>
</BasicButton>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script lang="ts" setup>
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import type { AppNavItem } from '../types'
defineProps<{
visible: boolean
overflowItems: AppNavItem[]
}>()
defineEmits<{
close: []
navigate: [routeName: string]
}>()
</script>
+145
View File
@@ -0,0 +1,145 @@
.bottom-appbar {
z-index: var(--app-z-nav, 5000);
height: var(--app-nav-height, 80px);
position: fixed;
display: flex;
justify-content: space-evenly;
background: #fff;
bottom: var(--app-credit-footer-offset, 28px);
right: 0;
left: 0;
width: 100%;
max-width: 100vw;
box-sizing: border-box;
@media screen and (min-width: 426px) {
width: 426px;
max-width: 426px;
left: 50%;
right: auto;
transform: translateX(-50%);
}
.tabs {
display: flex;
height: 100%;
width: 100%;
background: #fff;
justify-content: space-between;
position: absolute;
bottom: 0;
z-index: 400;
box-shadow: 0 -2px 4px -2px #0000001a;
.tab {
width: 80px;
text-align: center;
flex-flow: column;
display: flex;
align-items: center;
justify-content: center;
align-content: center;
}
.main_in {
width: 70px;
height: 70px;
flex-shrink: 0;
align-self: center;
.circle_in {
margin-top: -10px;
border-radius: 50%;
width: 70px;
height: 70px;
flex-shrink: 0;
z-index: 20;
display: flex;
justify-content: center;
align-items: center;
align-content: center;
flex-wrap: wrap;
background-color: @App-primary-color;
.circle_into {
overflow: hidden;
@apply bg-primary;
border-radius: 50%;
width: 56px;
height: 56px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-content: center;
flex-wrap: wrap;
border: white 1px solid;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.circle_in.is-white-nav-logo {
.circle_into {
background: #fff !important;
border-color: rgba(0, 0, 0, 0.08);
}
}
}
}
.main {
height: 70px;
width: 70px;
position: relative;
.circle {
border-radius: 50%;
height: 70px;
width: 70px;
z-index: 20;
box-shadow: 0 -5px 4px -2px #0000001a;
position: absolute;
}
}
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.4s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
.main-with-bottom-chrome {
height: calc(100vh - 72px - var(--app-credit-footer-offset, 28px));
}
.main-with-bottom-chrome-inner {
height: calc(100vh - 72px - var(--app-credit-footer-offset, 28px));
}
.main-with-bottom-chrome--no-nav,
.main-with-bottom-chrome--no-nav .main-with-bottom-chrome-inner {
height: calc(100vh - var(--app-credit-footer-offset, 28px));
}
+126
View File
@@ -0,0 +1,126 @@
@desktop-layout-min: 768px;
@mobile-frame-max: 767.98px;
/**
* Full-width desktop shell — applies on first paint (no JS class needed).
* Sidebar-specific wrap-fix offset stays on html.app-layout--desktop.
*/
@media screen and (min-width: @desktop-layout-min) {
html {
width: 100%;
overflow-x: hidden;
}
body {
display: block;
width: 100%;
min-width: 100%;
margin: 0;
padding: 0;
}
#app {
width: 100%;
max-width: none;
min-height: 100vh;
}
.desktopConfig {
position: static !important;
width: 100% !important;
max-width: none !important;
min-height: 100vh;
left: auto !important;
right: auto !important;
}
.wrap-fix {
width: auto !important;
max-width: none !important;
left: 0 !important;
right: 0 !important;
}
:root {
--app-credit-bar-inner: 0px;
--app-credit-footer-offset: 0px;
}
.app-credit-footer {
display: none !important;
}
/* Teleport to body escapes parent v-show — hide mobile dock chrome on desktop */
.app-dock-add-bar,
.wallet-card-slider {
display: none !important;
}
.transaction-list,
.card-slider-wrap {
width: 100% !important;
max-width: none !important;
left: 0 !important;
right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
}
html.app-layout--desktop {
@media screen and (min-width: @desktop-layout-min) {
.wrap-fix {
right: var(--app-desktop-sidebar-width, 260px) !important;
}
}
&.app-layout--desktop-no-sidebar .wrap-fix {
right: 0 !important;
}
}
/* Splash — always full viewport; never sidebar-offset */
.app-splash-screen {
position: fixed;
inset: 0;
width: 100vw;
min-height: 100vh;
max-width: none;
left: 0 !important;
right: 0 !important;
z-index: 1000000000000;
&__gradient {
position: absolute;
left: 0;
right: 0;
width: 100%;
&--top {
top: 0;
}
&--bottom {
bottom: 0;
}
}
}
html.app-layout--desktop .app-splash-screen {
right: 0 !important;
left: 0 !important;
width: 100vw !important;
}
/* Login — full viewport on desktop */
html.app-layout--desktop {
.login-page-root,
.dauth {
width: 100% !important;
min-height: 100vh;
}
.dauth__layout {
min-height: 100vh;
}
}
+84
View File
@@ -0,0 +1,84 @@
/**
* Mobile shell width rules.
* Below 426px: full viewport (phones).
* 426px–767px: centered 426px frame (tablet-ish mobile).
*/
html:not(.app-layout--desktop) {
/* Teleport to body — dock bar must span viewport, not shrink to content */
.app-dock-add-bar,
.wallet-card-slider {
box-sizing: border-box;
}
@media screen and (max-width: 425.98px) {
body {
display: block !important;
width: 100% !important;
overflow-x: hidden;
}
#app,
.desktopConfig,
.app-main,
.main-with-bottom-chrome,
.main-with-bottom-chrome-inner {
width: 100% !important;
max-width: 100vw !important;
min-width: 0 !important;
margin: 0 !important;
}
.wrap-fix,
.bottom-appbar {
width: 100% !important;
max-width: 100vw !important;
left: 0 !important;
right: 0 !important;
margin: 0 !important;
transform: none !important;
}
.app-dock-add-bar,
.wallet-card-slider {
width: 100% !important;
max-width: 100vw !important;
left: 0 !important;
right: 0 !important;
margin: 0 !important;
transform: none !important;
}
}
@media screen and (min-width: 426px) and (max-width: 767.98px) {
body {
display: flex !important;
justify-content: center;
}
.desktopConfig {
width: 426px !important;
max-width: 426px !important;
}
.wrap-fix,
.bottom-appbar {
width: 426px !important;
max-width: 426px !important;
left: 50% !important;
right: auto !important;
margin: 0 !important;
transform: translateX(-50%) !important;
}
.app-dock-add-bar,
.wallet-card-slider {
width: 426px !important;
max-width: 426px !important;
left: 50% !important;
right: auto !important;
margin: 0 !important;
transform: translateX(-50%) !important;
}
}
}
+6
View File
@@ -0,0 +1,6 @@
export interface AppNavItem {
id: string
routeName: string
label: string
icon: string
}