Files
CRM_Montzar/apps/web/src/layouts/AppLayout.vue
T

2214 lines
52 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
import { api } from '@/lib/api';
import { formatJalaliShort, toFaDigits } from '@/lib/jalali';
import UiIcon from '@/components/UiIcon.vue';
import MentoLogo from '@/components/brand/MentoLogo.vue';
import QuickCreate from '@/components/QuickCreate.vue';
import OrgSwitcher from '@/components/OrgSwitcher.vue';
import NavHubPanel from '@/components/nav/NavHubPanel.vue';
import { NAV_HUBS, type NavHubDef, type NavHubKey } from '@/lib/nav-hubs';
import type {
ActivitySummaryDto,
NotificationDto,
NotificationsListDto,
} from '@mento/shared';
import { canManageSettings, canViewAllRecords } from '@/lib/roles';
const auth = useAuthStore();
const router = useRouter();
const route = useRoute();
const canSettings = computed(() => canManageSettings(auth.user?.role));
const showOwnRecordsBanner = computed(
() => Boolean(auth.user?.role) && !canViewAllRecords(auth.user?.role),
);
const todayLabel = computed(() => formatJalaliShort(new Date()));
const SIDEBAR_KEY = 'mento_sidebar_collapsed';
const collapsed = ref(false);
const openHubKey = ref<NavHubKey | null>(null);
const hubStack = ref<NavHubKey[]>([]);
const isMobileNav = ref(false);
const globalSearch = ref('');
const activityBadge = ref(0);
function pathIn(prefixes: string[]) {
return prefixes.some(
(p) => route.path === p || route.path.startsWith(`${p}/`),
);
}
const dashboardActive = computed(() => route.path === '/');
const loyaltyActive = computed(() => pathIn(['/loyalty']));
const reportsActive = computed(() => pathIn(['/reports']));
const leadsActive = computed(() =>
pathIn(['/leads', '/settings/lead-stages', '/settings/lead-forms']),
);
const customersActive = computed(
() => pathIn(['/contacts']) || (isMobileNav.value && leadsActive.value),
);
const salesActive = computed(() =>
pathIn([
'/pipeline',
'/deals',
'/quotes',
'/invoices',
'/commissions',
'/sales/forecast',
'/settings/products',
]),
);
const activitiesActive = computed(() => pathIn(['/activities']));
const notesActive = computed(() => route.path === '/notes' || route.path.startsWith('/notes/'));
const supportActive = computed(() => pathIn(['/tickets', '/conversations']));
const automationsActive = computed(
() =>
route.path.startsWith('/settings/automations') ||
route.path.startsWith('/settings/message-automations') ||
route.path.startsWith('/settings/sms-campaigns'),
);
const toolsActive = computed(() =>
pathIn([
'/projects',
'/project-cards',
'/inventory',
'/cheques',
'/attachments',
'/reservations',
]),
);
const settingsActive = computed(() =>
pathIn([
'/settings/team',
'/settings/audit-logs',
'/settings/public-api',
'/settings/custom-fields',
'/settings/currencies',
'/settings/tahesab',
'/settings/woocommerce',
'/settings/voip',
'/settings/conversations',
'/settings/faq',
]),
);
const moreSectionActive = computed(
() =>
notesActive.value ||
supportActive.value ||
loyaltyActive.value ||
reportsActive.value ||
automationsActive.value ||
toolsActive.value ||
settingsActive.value,
);
const enabledModules = computed(() => auth.user?.enabledModules ?? null);
const subscription = computed(() => auth.user?.subscription ?? null);
function moduleAllowed(module?: string) {
if (!module) return true;
if (!enabledModules.value) return true;
return enabledModules.value.includes(module as never);
}
const hubVisible = computed(() => {
const map: Record<string, boolean> = {};
for (const hub of NAV_HUBS) {
map[hub.key] = hub.cards.some(
(card) =>
(!card.admin || canSettings.value) && moduleAllowed(card.module),
);
}
return map;
});
const openHub = computed<NavHubDef | null>(() => {
if (!openHubKey.value) return null;
const hub = NAV_HUBS.find((item) => item.key === openHubKey.value);
if (!hub) return null;
const cards = hub.cards.filter(
(card) =>
(!card.admin || canSettings.value) && moduleAllowed(card.module),
);
if (
hub.key === 'customers' &&
isMobileNav.value &&
hubVisible.value.leads
) {
cards.push({
to: '/leads',
hub: 'leads',
title: 'لیدها',
desc: 'مخاطبین بالقوه قبل از تبدیل به مشتری',
icon: 'user-plus',
tint: 'sky',
module: 'leads',
});
}
return {
...hub,
cards,
shortcuts: hub.shortcuts.filter(
(item) =>
(!item.admin || canSettings.value) && moduleAllowed(item.module),
),
};
});
const hubCanBack = computed(() => hubStack.value.length > 0);
const profileFullName = computed(() => auth.user?.fullName?.trim() || '');
const profileDisplayName = computed(() => profileFullName.value);
function closeHub() {
openHubKey.value = null;
hubStack.value = [];
}
function onHubClick(key: NavHubKey) {
hubStack.value = [];
openHubKey.value = openHubKey.value === key ? null : key;
}
function openNestedHub(key: NavHubKey) {
if (openHubKey.value) {
hubStack.value = [...hubStack.value, openHubKey.value];
}
openHubKey.value = key;
}
function backHub() {
const prev = hubStack.value[hubStack.value.length - 1];
if (!prev) {
closeHub();
return;
}
hubStack.value = hubStack.value.slice(0, -1);
openHubKey.value = prev;
}
function openTabHub(key: NavHubKey) {
moreOpen.value = false;
notifOpen.value = false;
onHubClick(key);
}
function openMoreHub(key: NavHubKey) {
notifOpen.value = false;
onHubClick(key);
}
function goHub(to: string) {
closeHub();
closeMenu();
void router.push(to);
}
const moreOpen = ref(false);
const notifOpen = ref(false);
const loading = ref(false);
const unreadCount = ref(0);
const items = ref<NotificationDto[]>([]);
let pollTimer: ReturnType<typeof setInterval> | null = null;
let mobileMq: MediaQueryList | null = null;
const hasUnread = computed(() => unreadCount.value > 0);
const moreTabActive = computed(() => moreOpen.value || moreSectionActive.value);
function closeMore() {
moreOpen.value = false;
}
function closeMenu() {
closeMore();
}
function toggleCollapsed() {
collapsed.value = !collapsed.value;
try {
localStorage.setItem(SIDEBAR_KEY, collapsed.value ? '1' : '0');
} catch {
// ignore
}
}
function closeNotif() {
notifOpen.value = false;
}
function onMobileMqChange(event: MediaQueryListEvent) {
isMobileNav.value = event.matches;
if (!event.matches) {
closeMore();
hubStack.value = [];
}
}
function onLogout() {
closeHub();
closeMenu();
closeNotif();
auth.logout();
router.push({ name: 'login' });
}
async function refreshNotifs() {
if (!auth.isAuthenticated) return;
loading.value = true;
try {
const { data } = await api.get<NotificationsListDto>('/notifications', {
params: { limit: 20 },
});
items.value = data.items;
unreadCount.value = data.unreadCount;
} catch {
// keep previous
} finally {
loading.value = false;
}
}
async function loadActivityBadge() {
try {
const { data } = await api.get<ActivitySummaryDto>('/activities/summary');
activityBadge.value = data.todayCount + data.overdueCount;
} catch {
activityBadge.value = 0;
}
}
async function markRead(item: NotificationDto) {
if (!item.readAt) {
await api.patch(`/notifications/${item.id}/read`);
item.readAt = new Date().toISOString();
unreadCount.value = Math.max(0, unreadCount.value - 1);
}
closeNotif();
closeMenu();
if (item.link) await router.push(item.link);
}
async function markAll() {
await api.patch('/notifications/read-all');
items.value = items.value.map((n) => ({
...n,
readAt: n.readAt ?? new Date().toISOString(),
}));
unreadCount.value = 0;
}
function toggleMore() {
notifOpen.value = false;
if (moreOpen.value) {
moreOpen.value = false;
return;
}
closeHub();
moreOpen.value = true;
}
function toggleNotif() {
moreOpen.value = false;
notifOpen.value = !notifOpen.value;
if (notifOpen.value) void refreshNotifs();
}
function formatTime(iso: string) {
try {
return new Intl.DateTimeFormat('fa-IR', {
dateStyle: 'short',
timeStyle: 'short',
}).format(new Date(iso));
} catch {
return iso;
}
}
function submitSearch() {
const q = globalSearch.value.trim();
if (!q) return;
void router.push({ name: 'contacts', query: { q } });
}
function syncBodyLock() {
const overlay = moreOpen.value || Boolean(openHubKey.value);
document.body.style.overflow =
overlay || notifOpen.value ? 'hidden' : '';
document.body.classList.toggle('mobile-nav-overlay', overlay);
}
watch(moreOpen, syncBodyLock);
watch(notifOpen, syncBodyLock);
watch(openHubKey, syncBodyLock);
watch(
() => route.fullPath,
() => {
closeMenu();
closeNotif();
closeHub();
},
);
onMounted(() => {
try {
collapsed.value = localStorage.getItem(SIDEBAR_KEY) === '1';
} catch {
collapsed.value = false;
}
void refreshNotifs();
void loadActivityBadge();
pollTimer = setInterval(() => void refreshNotifs(), 45000);
mobileMq = window.matchMedia('(max-width: 900px)');
isMobileNav.value = mobileMq.matches;
mobileMq.addEventListener('change', onMobileMqChange);
});
onUnmounted(() => {
if (pollTimer) clearInterval(pollTimer);
document.body.style.overflow = '';
document.body.classList.remove('mobile-nav-overlay');
mobileMq?.removeEventListener('change', onMobileMqChange);
});
</script>
<template>
<div class="shell" :class="{ collapsed, 'hub-open': Boolean(openHub), 'more-open': moreOpen }">
<aside id="app-sidebar" class="sidebar" :class="{ collapsed }">
<div class="brand-row">
<div class="brand-cluster">
<MentoLogo class="sidebar-logo" :size="52" />
<RouterLink
class="profile-card profile-brand"
:to="{ name: 'profile' }"
:title="auth.user?.fullName || 'پروفایل'"
@click="closeMenu"
>
<span class="avatar-wrap">
<span class="avatar">
<img
:src="auth.avatarSrc"
:alt="auth.user?.fullName || 'کاربر'"
width="36"
height="36"
/>
</span>
<i class="online-dot" title="آنلاین" aria-hidden="true" />
</span>
<span class="profile-meta">
<strong>{{ profileDisplayName }}</strong>
</span>
</RouterLink>
</div>
<button
class="icon-btn ghost mobile-only"
type="button"
aria-label="بستن منو"
@click="closeMenu"
>
✕
</button>
<button
class="collapse-btn desktop-only"
type="button"
:aria-pressed="collapsed"
:aria-label="collapsed ? 'باز کردن منو' : 'جمع کردن منو'"
:title="collapsed ? 'باز کردن منو' : 'جمع کردن منو'"
@click="toggleCollapsed"
>
<UiIcon name="chevron" :size="12" />
</button>
</div>
<div class="rail-stem">
<nav class="side-nav">
<div class="nav-primary">
<RouterLink to="/" class="nav-item" title="داشبورد" @click="closeHub(); closeMenu()">
<UiIcon name="home" :size="collapsed ? 20 : 18" />
<span>داشبورد</span>
</RouterLink>
<div class="nav-fold" v-if="hubVisible.customers" :class="{ active: customersActive || openHubKey === 'customers' }">
<button
class="nav-item nav-fold-btn"
type="button"
title="مشتریان"
:class="{ 'router-link-active': customersActive || openHubKey === 'customers', 'hub-on': openHubKey === 'customers' }"
:aria-expanded="openHubKey === 'customers'"
@click="onHubClick('customers')"
>
<UiIcon name="users" :size="collapsed ? 20 : 18" />
<span>مشتریان</span>
<i class="chev" aria-hidden="true" />
</button>
</div>
<div class="nav-fold" v-if="hubVisible.leads" :class="{ active: leadsActive || openHubKey === 'leads' }">
<button
class="nav-item nav-fold-btn"
type="button"
title="لیدها"
:class="{ 'router-link-active': leadsActive || openHubKey === 'leads', 'hub-on': openHubKey === 'leads' }"
:aria-expanded="openHubKey === 'leads'"
@click="onHubClick('leads')"
>
<UiIcon name="user-plus" :size="collapsed ? 20 : 18" />
<span>لیدها</span>
<i class="chev" aria-hidden="true" />
</button>
</div>
<div class="nav-fold" v-if="hubVisible.sales" :class="{ active: salesActive || openHubKey === 'sales' }">
<button
class="nav-item nav-fold-btn"
type="button"
title="فروش"
:class="{ 'router-link-active': salesActive || openHubKey === 'sales', 'hub-on': openHubKey === 'sales' }"
:aria-expanded="openHubKey === 'sales'"
@click="onHubClick('sales')"
>
<UiIcon name="briefcase" :size="collapsed ? 20 : 18" />
<span>فروش</span>
<i class="chev" aria-hidden="true" />
</button>
</div>
<div class="nav-fold" v-if="hubVisible.activities" :class="{ active: activitiesActive || openHubKey === 'activities' }">
<button
class="nav-item nav-fold-btn"
type="button"
title="کارها"
:class="{ 'router-link-active': activitiesActive || openHubKey === 'activities', 'hub-on': openHubKey === 'activities' }"
:aria-expanded="openHubKey === 'activities'"
@click="onHubClick('activities')"
>
<UiIcon name="check" :size="collapsed ? 20 : 18" />
<span>کارها</span>
<span v-if="activityBadge" class="nav-badge">{{ toFaDigits(activityBadge) }}</span>
<i class="chev" aria-hidden="true" />
</button>
</div>
<RouterLink
to="/notes"
class="nav-item"
title="یادداشت‌ها"
:class="{ 'router-link-active': notesActive }"
@click="closeHub(); closeMenu()"
>
<UiIcon name="document" :size="collapsed ? 20 : 18" />
<span>یادداشت‌ها</span>
</RouterLink>
<div class="nav-fold" :class="{ active: supportActive || openHubKey === 'support' }">
<button
class="nav-item nav-fold-btn"
type="button"
title="پشتیبانی"
:class="{ 'router-link-active': supportActive || openHubKey === 'support', 'hub-on': openHubKey === 'support' }"
:aria-expanded="openHubKey === 'support'"
@click="onHubClick('support')"
>
<UiIcon name="chat" :size="collapsed ? 20 : 18" />
<span>پشتیبانی</span>
<i class="chev" aria-hidden="true" />
</button>
</div>
<RouterLink to="/loyalty" class="nav-item" title="باشگاه مشتریان" @click="closeHub(); closeMenu()">
<UiIcon name="gift" :size="collapsed ? 20 : 18" />
<span>باشگاه مشتریان</span>
</RouterLink>
<RouterLink to="/reports" class="nav-item" title="گزارش‌ها" @click="closeHub(); closeMenu()">
<UiIcon name="chart" :size="collapsed ? 20 : 18" />
<span>گزارش‌ها</span>
</RouterLink>
<div
v-if="canSettings"
class="nav-fold"
:class="{ active: automationsActive || openHubKey === 'automations' }"
>
<button
class="nav-item nav-fold-btn"
type="button"
title="اتوماسیون"
:class="{ 'router-link-active': automationsActive || openHubKey === 'automations', 'hub-on': openHubKey === 'automations' }"
:aria-expanded="openHubKey === 'automations'"
@click="onHubClick('automations')"
>
<UiIcon name="layers" :size="collapsed ? 20 : 18" />
<span>اتوماسیون</span>
<i class="chev" aria-hidden="true" />
</button>
</div>
</div>
<div class="nav-sep" aria-hidden="true" />
<div class="nav-utility">
<div class="nav-fold" :class="{ active: toolsActive || openHubKey === 'tools' }">
<button
class="nav-item nav-fold-btn"
type="button"
title="ابزارها"
:class="{ 'router-link-active': toolsActive || openHubKey === 'tools', 'hub-on': openHubKey === 'tools' }"
:aria-expanded="openHubKey === 'tools'"
@click="onHubClick('tools')"
>
<UiIcon name="apps-plus" :size="collapsed ? 20 : 18" />
<span>ابزارها</span>
<i class="chev" aria-hidden="true" />
</button>
</div>
<div
v-if="canSettings"
class="nav-fold"
:class="{ active: settingsActive || openHubKey === 'settings' }"
>
<button
class="nav-item nav-fold-btn"
type="button"
title="تنظیمات"
:class="{ 'router-link-active': settingsActive || openHubKey === 'settings', 'hub-on': openHubKey === 'settings' }"
:aria-expanded="openHubKey === 'settings'"
@click="onHubClick('settings')"
>
<UiIcon name="settings" :size="collapsed ? 20 : 18" />
<span>تنظیمات</span>
<i class="chev" aria-hidden="true" />
</button>
</div>
</div>
</nav>
<p v-if="showOwnRecordsBanner" class="scope-banner">
فقط پرونده‌های خودتان را می‌بینید.
</p>
<div class="sidebar-foot">
<div class="foot-actions">
<RouterLink
class="profile-card profile-foot"
:to="{ name: 'profile' }"
:title="auth.user?.fullName || 'پروفایل'"
@click="closeMenu"
>
<span class="avatar-wrap">
<span class="avatar">
<img
:src="auth.avatarSrc"
:alt="auth.user?.fullName || 'کاربر'"
width="36"
height="36"
/>
</span>
<i class="online-dot" title="آنلاین" aria-hidden="true" />
</span>
<span class="profile-meta">
<strong>{{ profileDisplayName }}</strong>
</span>
</RouterLink>
<div class="foot-meta">
<OrgSwitcher :compact="collapsed" />
<button
class="logout-link"
type="button"
title="خروج از حساب"
aria-label="خروج از حساب"
@click="onLogout"
>
<UiIcon name="logout" :size="18" />
<span>خروج از حساب</span>
</button>
</div>
</div>
</div>
</div>
</aside>
<div class="workspace">
<header class="mobile-topbar mobile-only">
<div class="top-brand">
<MentoLogo :size="32" :show-text="false" variant="mark" />
<BrandName lang="en" />
</div>
<div class="top-tools">
<button class="round-btn" type="button" aria-label="اعلان‌ها" @click="toggleNotif">
<UiIcon name="bell" :size="18" />
<span v-if="hasUnread" class="badge">{{ toFaDigits(unreadCount) }}</span>
</button>
<RouterLink
class="top-avatar"
:to="{ name: 'profile' }"
:title="auth.user?.fullName || 'پروفایل'"
:aria-label="auth.user?.fullName || 'پروفایل'"
>
<span class="avatar-wrap">
<span class="avatar">
<img
:src="auth.avatarSrc"
:alt="auth.user?.fullName || 'کاربر'"
width="36"
height="36"
/>
</span>
<i class="online-dot" title="آنلاین" aria-hidden="true" />
</span>
</RouterLink>
</div>
</header>
<header class="desktop-topbar desktop-only">
<form class="global-search" @submit.prevent="submitSearch">
<UiIcon name="search" :size="18" class="search-leading" />
<input
v-model="globalSearch"
type="search"
placeholder="جستجو در مشتریان، معاملات و..."
/>
</form>
<div class="topbar-actions">
<RouterLink class="round-btn accent" :to="{ name: 'notes' }" title="هوش مصنوعی">
<UiIcon name="sparkle" :size="18" />
</RouterLink>
<button class="round-btn" type="button" aria-label="اعلان‌ها" @click="toggleNotif">
<UiIcon name="bell" :size="18" />
<span v-if="hasUnread" class="badge">{{ toFaDigits(unreadCount) }}</span>
</button>
<RouterLink class="round-btn" :to="{ name: 'activities-calendar' }" title="تقویم">
<UiIcon name="calendar" :size="18" />
</RouterLink>
<RouterLink class="round-btn" :to="{ name: 'tickets' }" title="پیام‌ها">
<UiIcon name="mail" :size="18" />
</RouterLink>
<div
v-if="subscription"
class="sub-chip"
:class="{ warn: subscription.warn }"
:title="`انقضا: ${subscription.expiresAt}`"
>
<UiIcon name="clock" :size="15" />
<span>
{{ toFaDigits(subscription.daysRemaining) }} روز باقی‌مانده
</span>
</div>
<div class="date-chip">
<UiIcon name="calendar" :size="16" />
<span>{{ todayLabel }}</span>
</div>
</div>
</header>
<div v-if="notifOpen" class="notif-layer">
<div class="notif-backdrop" @click="closeNotif" />
<div class="notif-panel" role="dialog" aria-label="اعلان‌ها">
<header class="notif-head">
<strong>اعلان‌ها</strong>
<div class="notif-head-actions">
<button v-if="hasUnread" class="linkish" type="button" @click="markAll">
همه خوانده شد
</button>
<button class="round-btn sm" type="button" aria-label="بستن" @click="closeNotif">
✕
</button>
</div>
</header>
<p v-if="loading && !items.length" class="muted">در حال بارگذاری…</p>
<ul v-else-if="items.length" class="notif-list">
<li v-for="item in items" :key="item.id" :class="{ unread: !item.readAt }">
<button type="button" @click="markRead(item)">
<span class="title">{{ item.title }}</span>
<span v-if="item.body" class="body">{{ item.body }}</span>
<span class="time">{{ formatTime(item.createdAt) }}</span>
</button>
</li>
</ul>
<p v-else class="muted">اعلانی نیست.</p>
</div>
</div>
<main class="content">
<RouterView />
</main>
</div>
<div v-if="openHub" class="hub-layer">
<button class="hub-dim" type="button" aria-label="بستن منو" @click="closeHub" />
<NavHubPanel
:hub="openHub"
:can-back="hubCanBack"
@close="closeHub"
@back="backHub"
@go="goHub"
@open-hub="openNestedHub"
/>
</div>
<nav class="mobile-tabbar mobile-only" aria-label="منوی اصلی">
<RouterLink
to="/"
class="tab"
:class="{ on: dashboardActive && !moreOpen }"
@click="closeHub(); closeMore()"
>
<UiIcon name="home" :size="20" />
<span>داشبورد</span>
</RouterLink>
<button
class="tab"
type="button"
:class="{
on:
!moreOpen &&
(customersActive ||
openHubKey === 'customers' ||
(isMobileNav && openHubKey === 'leads')),
}"
:aria-expanded="
openHubKey === 'customers' || (isMobileNav && openHubKey === 'leads')
"
@click="openTabHub('customers')"
>
<UiIcon name="users" :size="20" />
<span>مشتریان</span>
</button>
<button
class="tab"
type="button"
:class="{ on: !moreOpen && (salesActive || openHubKey === 'sales') }"
:aria-expanded="openHubKey === 'sales'"
@click="openTabHub('sales')"
>
<UiIcon name="briefcase" :size="20" />
<span>فروش</span>
</button>
<button
class="tab"
type="button"
:class="{ on: !moreOpen && (activitiesActive || openHubKey === 'activities') }"
:aria-expanded="openHubKey === 'activities'"
@click="openTabHub('activities')"
>
<span class="tab-ico">
<UiIcon name="check" :size="20" />
<span v-if="activityBadge" class="tab-badge">{{ toFaDigits(activityBadge) }}</span>
</span>
<span>کارها</span>
</button>
<button
class="tab"
type="button"
:class="{ on: moreTabActive }"
:aria-expanded="moreOpen"
aria-controls="mobile-more"
@click="toggleMore"
>
<UiIcon name="grid" :size="20" />
<span>بیشتر</span>
</button>
</nav>
<div
v-if="moreOpen"
id="mobile-more"
class="more-layer mobile-only"
role="dialog"
aria-modal="true"
aria-label="منو"
>
<header class="more-head">
<h2>منو</h2>
<button class="round-btn sm" type="button" aria-label="بستن" @click="closeMore">✕</button>
</header>
<nav class="more-body">
<RouterLink to="/notes" class="nav-item" @click="closeHub(); closeMore()">
<UiIcon name="document" :size="18" />
<span>یادداشت‌ها</span>
</RouterLink>
<button
class="nav-item nav-fold-btn"
type="button"
:class="{
'router-link-active': supportActive || openHubKey === 'support',
'hub-on': openHubKey === 'support',
}"
:aria-expanded="openHubKey === 'support'"
@click="openMoreHub('support')"
>
<UiIcon name="chat" :size="18" />
<span>پشتیبانی</span>
<i class="chev" aria-hidden="true" />
</button>
<RouterLink to="/loyalty" class="nav-item" @click="closeHub(); closeMore()">
<UiIcon name="gift" :size="18" />
<span>باشگاه مشتریان</span>
</RouterLink>
<RouterLink to="/reports" class="nav-item" @click="closeHub(); closeMore()">
<UiIcon name="chart" :size="18" />
<span>گزارش‌ها</span>
</RouterLink>
<button
v-if="canSettings"
class="nav-item nav-fold-btn"
type="button"
:class="{
'router-link-active': automationsActive || openHubKey === 'automations',
'hub-on': openHubKey === 'automations',
}"
:aria-expanded="openHubKey === 'automations'"
@click="openMoreHub('automations')"
>
<UiIcon name="layers" :size="18" />
<span>اتوماسیون</span>
<i class="chev" aria-hidden="true" />
</button>
<button
class="nav-item nav-fold-btn"
type="button"
:class="{
'router-link-active': toolsActive || openHubKey === 'tools',
'hub-on': openHubKey === 'tools',
}"
:aria-expanded="openHubKey === 'tools'"
@click="openMoreHub('tools')"
>
<UiIcon name="apps-plus" :size="18" />
<span>ابزارها</span>
<i class="chev" aria-hidden="true" />
</button>
<button
v-if="canSettings"
class="nav-item nav-fold-btn"
type="button"
:class="{
'router-link-active': settingsActive || openHubKey === 'settings',
'hub-on': openHubKey === 'settings',
}"
:aria-expanded="openHubKey === 'settings'"
@click="openMoreHub('settings')"
>
<UiIcon name="settings" :size="18" />
<span>تنظیمات</span>
<i class="chev" aria-hidden="true" />
</button>
</nav>
<button class="logout-link more-logout" type="button" @click="onLogout">
<UiIcon name="logout" :size="18" />
<span>خروج از حساب</span>
</button>
</div>
<QuickCreate />
</div>
</template>
<style scoped>
.shell {
--sidebar-card: 220px;
--sidebar-gutter-start: 14px;
--sidebar-gutter-end: 10px;
--hub-offset: calc(var(--sidebar-card) + var(--sidebar-gutter-start));
display: grid;
grid-template-columns: calc(var(--sidebar-card) + var(--sidebar-gutter-start) + var(--sidebar-gutter-end)) minmax(0, 1fr);
min-height: 100vh;
min-height: 100dvh;
background: var(--bg);
gap: 0;
transition: grid-template-columns 0.2s ease;
}
.workspace {
min-width: 0;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
}
.backdrop,
.mobile-only {
display: none;
}
.desktop-only {
display: flex;
}
.sidebar {
position: sticky;
top: 14px;
align-self: start;
z-index: 20;
height: calc(100dvh - 28px);
margin: 14px;
margin-inline-end: 10px;
background: #fff;
border: 0;
border-radius: 28px;
padding: 0.35rem 0.85rem 1.1rem;
display: flex;
flex-direction: column;
gap: 0.65rem;
overflow: hidden;
box-sizing: border-box;
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.06);
}
.rail-stem {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
gap: 0.85rem;
}
.brand-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.45rem;
padding: 0.1rem 0.2rem 0.2rem;
flex-shrink: 0;
}
.brand-cluster {
display: flex;
align-items: center;
gap: 1.5rem;
min-width: 0;
flex: 1;
}
.brand-cluster :deep(.brand-logo.full) {
gap: 0.55rem;
flex-shrink: 0;
min-width: max-content;
}
.sidebar-logo {
flex-shrink: 0;
}
.brand-cluster :deep(.text) {
flex-shrink: 0;
}
.brand-cluster :deep(.text strong) {
font-size: 1.28rem;
white-space: nowrap;
}
.brand-cluster :deep(.text small) {
font-size: 0.82rem;
white-space: nowrap;
}
.side-nav {
display: grid;
gap: 0.18rem;
align-content: start;
overflow-y: auto;
min-height: 0;
flex: 1;
scrollbar-width: none;
padding-inline: 0.15rem;
}
.side-nav::-webkit-scrollbar {
display: none;
}
.nav-primary,
.nav-sub {
display: grid;
gap: 0.2rem;
align-content: start;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.62rem 0.9rem;
border-radius: 16px;
color: #334155;
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
transition: background 0.15s ease, color 0.15s ease;
}
.nav-item:hover {
background: #f8fafc;
color: #0f172a;
}
.nav-item.router-link-active {
background: #d8faf3;
color: #0f766e;
box-shadow: none;
}
.nav-item.router-link-active :deep(.ico) {
color: #0f766e;
}
.shell.hub-open .nav-item.router-link-active:not(.hub-on) {
background: transparent;
color: #334155;
box-shadow: none;
}
.shell.hub-open .nav-item.router-link-active:not(.hub-on) :deep(.ico) {
color: inherit;
}
.shell.hub-open .sidebar {
z-index: 70;
}
.hub-layer {
position: fixed;
inset: 0;
z-index: 60;
pointer-events: none;
}
.hub-dim {
pointer-events: auto;
position: absolute;
inset: 0;
border: 0;
padding: 0;
background: rgba(15, 23, 42, 0.28);
cursor: default;
}
.hub-layer :deep(.hub) {
pointer-events: auto;
position: absolute;
top: 42px;
inset-inline-start: var(--hub-offset);
inset-inline-end: auto;
}
.nav-badge {
margin-inline-start: auto;
min-width: 1.35rem;
height: 1.35rem;
border-radius: 999px;
background: #14b8a6;
color: #fff;
font-size: 0.7rem;
display: grid;
place-items: center;
padding: 0 0.3rem;
}
.nav-item.router-link-active .nav-badge {
background: #14b8a6;
color: #fff;
}
.nav-fold {
display: grid;
gap: 0.15rem;
}
.nav-fold-btn {
width: 100%;
border: 0;
background: transparent;
cursor: pointer;
font: inherit;
text-align: right;
}
.nav-fold-btn .chev {
margin-inline-start: auto;
}
.nav-sep {
height: 1px;
margin: 0.35rem 0.85rem;
background: #e8eef4;
}
.nav-utility {
display: grid;
gap: 0.2rem;
align-content: start;
}
.chev {
width: 0.42rem;
height: 0.42rem;
border-right: 1.6px solid currentColor;
border-bottom: 1.6px solid currentColor;
transform: rotate(135deg);
transition: transform 0.15s ease;
flex-shrink: 0;
}
.nav-fold-btn.hub-on .chev {
transform: rotate(-45deg);
}
.nav-sub {
margin-inline-end: 0.55rem;
padding-inline-end: 0.25rem;
border-inline-end: 1px solid #eef2f7;
}
.nav-sub a {
display: block;
padding: 0.58rem 0.9rem;
border-radius: 14px;
color: #64748b;
text-decoration: none;
font-size: 0.88rem;
}
.nav-sub a.router-link-active {
background: #ecfdf8;
color: #0f766e;
font-weight: 700;
}
.scope-banner {
margin: 0;
padding: 0.55rem 0.75rem;
border-radius: 12px;
background: #f8fafc;
color: #94a3b8;
font-size: 0.75rem;
}
.sidebar-foot {
margin-top: auto;
display: grid;
gap: 0.85rem;
flex-shrink: 0;
padding-top: 0.35rem;
}
.foot-actions {
display: grid;
gap: 0.55rem;
}
.foot-meta {
display: grid;
gap: 0.15rem;
}
.profile-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.22rem;
padding: 0.15rem 0.2rem;
border-radius: 12px;
background: transparent;
color: inherit;
text-decoration: none;
flex: 1 1 0;
min-width: 0;
max-width: 4.8rem;
}
.profile-foot {
display: none;
}
.profile-card:hover {
background: #f0fdfa;
}
.profile-card.router-link-active {
background: #ecfdf5;
}
.avatar-wrap {
position: relative;
width: 36px;
height: 36px;
flex-shrink: 0;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 999px;
overflow: hidden;
box-shadow: 0 3px 8px rgba(15, 23, 42, 0.1);
display: block;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.profile-meta {
min-width: 0;
width: 100%;
display: grid;
text-align: center;
}
.profile-meta strong {
font-size: 0.78rem;
font-weight: 800;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.online-dot {
position: absolute;
bottom: 0;
inset-inline-start: 0;
width: 10px;
height: 10px;
border-radius: 999px;
background: #22c55e;
border: 2px solid #fff;
box-sizing: border-box;
}
.logout-link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
border: 0;
background: transparent;
color: #94a3b8;
font: inherit;
font-size: 0.78rem;
cursor: pointer;
text-align: right;
padding: 0 0.45rem 0.15rem;
}
.collapse-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
min-height: 0;
padding: 0;
border: 1px solid #e2e8f0;
background: #fff;
color: #94a3b8;
font: inherit;
cursor: pointer;
border-radius: 999px;
flex-shrink: 0;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
transition:
background 0.15s ease,
color 0.15s ease,
border-color 0.15s ease,
box-shadow 0.15s ease;
}
.collapse-btn:hover {
background: #f8fafc;
color: #0f172a;
border-color: #cbd5e1;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
}
.collapse-btn :deep(.ico) {
width: 12px;
height: 12px;
transition: transform 0.15s ease;
transform: scaleX(-1);
}
.more-btn {
width: 100%;
border: 0;
background: transparent;
cursor: pointer;
font: inherit;
color: inherit;
}
.more-flyout {
position: absolute;
top: 4.6rem;
inset-inline-start: calc(100% + 8px);
inset-inline-end: auto;
width: 13.5rem;
max-height: min(70vh, 28rem);
overflow: auto;
background: #fff;
border: 1px solid #e8eef3;
border-radius: 16px;
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.12);
padding: 0.55rem;
display: grid;
gap: 0.15rem;
z-index: 40;
}
.more-flyout p {
margin: 0.35rem 0 0.15rem;
padding: 0 0.55rem;
color: #94a3b8;
font-size: 0.72rem;
font-weight: 800;
}
.more-flyout p:first-child {
margin-top: 0;
}
.more-flyout a {
display: block;
padding: 0.48rem 0.55rem;
border-radius: 10px;
color: #334155;
text-decoration: none;
font-size: 0.84rem;
}
.more-flyout a.router-link-active {
background: #ecfdf8;
color: #0f766e;
font-weight: 700;
}
@media (min-width: 901px) {
.sidebar:not(.collapsed) .profile-brand {
display: none;
}
.sidebar:not(.collapsed) .brand-cluster {
gap: 0;
}
.sidebar:not(.collapsed) .profile-foot {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.65rem;
max-width: none;
flex: 1 1 auto;
width: 100%;
padding: 0.45rem 0.55rem;
border-radius: 14px;
background: #f8fafc;
border: 1px solid #eef2f7;
}
.sidebar:not(.collapsed) .profile-foot:hover {
background: #f0fdfa;
border-color: #ccfbf1;
}
.sidebar:not(.collapsed) .profile-foot .profile-meta {
text-align: start;
flex: 1;
min-width: 0;
}
.sidebar:not(.collapsed) .profile-foot .profile-meta strong {
font-size: 0.84rem;
}
.sidebar:not(.collapsed) .foot-meta {
display: flex;
align-items: center;
gap: 0.45rem;
}
.sidebar:not(.collapsed) .foot-meta :deep(.org-switch) {
flex: 1;
min-width: 0;
}
.sidebar:not(.collapsed) .logout-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
border-radius: 12px;
background: #fff;
border: 1px solid #e2e8f0;
color: #64748b;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
flex-shrink: 0;
transition:
background 0.15s ease,
color 0.15s ease,
border-color 0.15s ease,
box-shadow 0.15s ease;
}
.sidebar:not(.collapsed) .logout-link:hover {
background: #fef2f2;
border-color: #fecaca;
color: #dc2626;
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.12);
}
.sidebar:not(.collapsed) .logout-link span {
display: none;
}
.shell.collapsed {
--hub-offset: 72px;
grid-template-columns: 80px minmax(0, 1fr);
}
.sidebar.collapsed {
width: 64px;
margin-inline: 8px;
padding: 0;
gap: 0;
align-items: flex-start;
overflow: visible;
background: transparent;
box-shadow: none;
border-radius: 0;
z-index: 24;
}
.sidebar.collapsed .brand-row {
width: max-content;
max-width: none;
height: var(--control-h);
min-height: var(--control-h);
box-sizing: border-box;
overflow: hidden;
align-self: flex-start;
align-items: center;
justify-content: flex-start;
z-index: 3;
padding: 0 0.5rem;
background: #fff;
border-radius: var(--radius);
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
}
.sidebar.collapsed .brand-cluster {
flex: 0 0 auto;
width: max-content;
flex-direction: row;
align-items: center;
gap: 0.55rem;
}
.sidebar.collapsed .sidebar-logo,
.sidebar.collapsed .brand-cluster :deep(.brand-logo) {
display: inline-flex;
flex-shrink: 0;
}
.sidebar.collapsed .brand-cluster :deep(.mark) {
width: 34px !important;
height: 34px !important;
}
.sidebar.collapsed .brand-cluster :deep(.text strong) {
font-size: 1rem;
line-height: 1.05;
}
.sidebar.collapsed .brand-cluster :deep(.text small) {
font-size: 0.7rem;
line-height: 1;
}
.sidebar.collapsed .profile-card {
flex-direction: row;
align-items: center;
max-width: none;
flex: 0 0 auto;
gap: 0;
padding: 0;
}
.sidebar.collapsed .profile-meta {
display: none;
}
.sidebar.collapsed .avatar-wrap,
.sidebar.collapsed .avatar {
width: 28px;
height: 28px;
}
.sidebar.collapsed .online-dot {
width: 8px;
height: 8px;
border-width: 1.5px;
}
.shell.collapsed .desktop-topbar {
padding-inline-start: 8rem;
}
.shell.collapsed .global-search {
margin-inline-start: 0;
}
.sidebar.collapsed .rail-stem {
width: 64px;
align-self: flex-start;
flex: 1 1 0;
min-height: 0;
overflow: hidden;
margin-top: 8px;
padding: 0.7rem 0.2rem 0.3rem;
background: #fff;
border-radius: 16px;
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
gap: 0.1rem;
z-index: 1;
}
.sidebar.collapsed .side-nav {
overflow-x: hidden;
overflow-y: auto;
min-height: 0;
flex: 1 1 0;
gap: 0.08rem;
padding-inline: 0;
scrollbar-width: none;
}
.sidebar.collapsed .nav-primary,
.sidebar.collapsed .nav-utility {
display: grid;
width: 100%;
gap: 0.08rem;
}
.sidebar.collapsed .nav-sep {
margin: 0.25rem 0.55rem;
}
.sidebar.collapsed .nav-fold {
gap: 0;
}
.sidebar.collapsed .nav-sub,
.sidebar.collapsed .nav-fold-btn .chev {
display: none;
}
.sidebar.collapsed .nav-item {
display: grid;
place-items: center;
flex-shrink: 0;
min-height: var(--control-h);
height: var(--control-h);
padding: 0;
border-radius: var(--radius);
position: relative;
}
.sidebar.collapsed .nav-item :deep(.ico) {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.sidebar.collapsed .nav-item > span:not(.nav-badge) {
display: none;
}
.sidebar.collapsed .nav-item.router-link-active {
background: #d8faf3;
color: #0f766e;
}
.sidebar.collapsed .nav-badge {
position: absolute;
top: 0.18rem;
inset-inline-end: 0.1rem;
margin: 0;
min-width: 0.95rem;
height: 0.95rem;
font-size: 0.58rem;
}
.sidebar.collapsed .scope-banner {
display: none;
}
.sidebar.collapsed .sidebar-foot {
margin-top: 0.15rem;
padding-top: 0;
gap: 0.1rem;
flex-shrink: 0;
}
.sidebar.collapsed .logout-link {
display: grid;
place-items: center;
width: 100%;
min-height: var(--control-h);
padding: 0;
border-radius: var(--radius);
color: #94a3b8;
}
.sidebar.collapsed .logout-link:hover {
background: #f8fafc;
color: #0f172a;
}
.sidebar.collapsed .logout-link span {
display: none;
}
.sidebar.collapsed .collapse-btn {
width: 20px;
height: 20px;
margin-inline-start: 0.15rem;
}
.sidebar.collapsed .collapse-btn :deep(.ico) {
transform: none;
}
}
.global-search {
width: min(100%, 420px);
max-width: 420px;
min-width: 0;
justify-self: start;
margin: 0;
display: flex;
align-items: center;
gap: 0.7rem;
background: #fff;
border-radius: var(--radius);
min-height: var(--control-h);
padding: 0 0.95rem;
border: 1px solid transparent;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
box-sizing: border-box;
}
.desktop-topbar {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 1rem;
min-width: 0;
/* همان فاصله بالای سایدبار شناور (14px) */
padding: 14px 1.5rem 0.85rem;
position: sticky;
top: 0;
z-index: 20;
background: color-mix(in srgb, var(--bg) 92%, transparent);
backdrop-filter: blur(10px);
}
.topbar-actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
/* مرکز عمودی نسبت به ارتفاع جستجو (~49px) */
min-height: var(--control-h);
}
.global-search:focus-within {
background: #fff;
border-color: #e2e8f0;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
}
.global-search input {
border: 0;
background: transparent;
width: 100%;
min-width: 0;
outline: none;
font-size: 0.92rem;
color: #0f172a;
}
.global-search input::placeholder {
color: #94a3b8;
}
.search-leading {
color: #94a3b8;
}
.round-btn {
position: relative;
width: var(--control-h);
height: var(--control-h);
border-radius: var(--radius);
border: 0;
background: #fff;
color: #64748b;
display: grid;
place-items: center;
cursor: pointer;
text-decoration: none;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.round-btn.accent {
color: #14b8a6;
background: #ecfdf8;
}
.round-btn.sm {
width: 34px;
height: 34px;
}
.sub-chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.7rem;
border-radius: 999px;
background: var(--accent-subtle);
color: var(--accent-deep);
font-size: 0.78rem;
font-weight: 800;
white-space: nowrap;
}
.sub-chip.warn {
background: color-mix(in srgb, var(--danger) 12%, white);
color: var(--danger);
}
.date-chip {
display: inline-flex;
align-items: center;
gap: 0.45rem;
height: var(--control-h);
box-sizing: border-box;
padding: 0 0.75rem;
border-radius: var(--radius);
background: #fff;
color: #64748b;
font-size: 0.84rem;
font-weight: 600;
white-space: nowrap;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.badge {
position: absolute;
top: -2px;
inset-inline-start: -2px;
min-width: 1.05rem;
height: 1.05rem;
border-radius: 999px;
background: #ef4444;
color: #fff;
font-size: 0.65rem;
display: grid;
place-items: center;
padding: 0 0.22rem;
border: 2px solid #fff;
}
.notif-layer {
position: fixed;
inset: 0;
z-index: 40;
display: grid;
place-items: start end;
padding: 5rem 1rem 1rem;
pointer-events: none;
}
.notif-backdrop {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.22);
pointer-events: auto;
}
.notif-panel {
position: relative;
width: min(360px, calc(100vw - 2rem));
max-height: min(70vh, 480px);
overflow: auto;
background: #fff;
border: 1px solid #e8eef3;
border-radius: 18px;
padding: 0.85rem;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.12);
pointer-events: auto;
}
.notif-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.55rem;
}
.notif-head-actions {
display: flex;
align-items: center;
gap: 0.35rem;
}
.linkish {
border: 0;
background: transparent;
color: #14b8a6;
cursor: pointer;
font: inherit;
font-size: 0.82rem;
}
.notif-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.35rem;
}
.notif-list button {
width: 100%;
text-align: right;
border: 0;
background: #f8fafc;
border-radius: 12px;
padding: 0.65rem 0.7rem;
cursor: pointer;
display: grid;
gap: 0.2rem;
font: inherit;
}
.notif-list li.unread button {
background: #ecfdf8;
}
.title {
font-weight: 700;
font-size: 0.9rem;
}
.body,
.time,
.muted {
color: #94a3b8;
font-size: 0.8rem;
}
.content {
padding: 0 1.5rem 1.5rem;
min-width: 0;
overflow-x: clip;
}
@media (max-width: 1280px) and (min-width: 901px) {
.content {
padding-inline: 1rem;
}
.desktop-topbar {
padding-inline: 1rem;
}
}
.burger {
width: 16px;
height: 2px;
background: currentColor;
position: relative;
display: block;
}
.burger::before,
.burger::after {
content: '';
position: absolute;
inset-inline: 0;
height: 2px;
background: currentColor;
}
.burger::before {
top: -5px;
}
.burger::after {
top: 5px;
}
.burger.open {
background: transparent;
}
.burger.open::before {
transform: translateY(5px) rotate(45deg);
}
.burger.open::after {
transform: translateY(-5px) rotate(-45deg);
}
.mobile-topbar {
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.75rem 0.9rem;
background: #fff;
border-bottom: 1px solid #e8eef3;
position: sticky;
top: 0;
z-index: 30;
}
.top-brand {
display: flex;
align-items: center;
gap: 0.5rem;
}
.top-tools {
display: flex;
align-items: center;
gap: 0.45rem;
}
.top-avatar {
display: grid;
place-items: center;
width: var(--control-h);
height: var(--control-h);
text-decoration: none;
flex-shrink: 0;
}
.top-avatar .avatar-wrap,
.top-avatar .avatar {
width: 36px;
height: 36px;
}
.mobile-tabbar,
.more-layer {
display: none;
}
@media (max-width: 900px) {
.shell {
--mobile-tabbar-h: calc(3.65rem + env(safe-area-inset-bottom, 0px));
grid-template-columns: minmax(0, 1fr);
}
.desktop-only {
display: none !important;
}
.mobile-only {
display: flex;
}
.sidebar {
display: none;
}
.hub-layer {
z-index: 80;
bottom: var(--mobile-tabbar-h);
}
.hub-layer :deep(.hub) {
top: auto;
bottom: 0;
inset-inline: 0;
width: 100%;
margin: 0;
padding-bottom: 0.9rem;
max-height: min(86dvh, calc(100dvh - var(--mobile-tabbar-h) - 2rem));
animation: hub-sheet-up 0.28s ease;
}
@keyframes hub-sheet-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.content {
padding: 1rem 0.85rem calc(1.25rem + var(--mobile-tabbar-h));
}
.notif-layer {
padding: 4.25rem 0.65rem calc(1rem + var(--mobile-tabbar-h));
place-items: start stretch;
}
.notif-panel {
width: 100%;
}
.mobile-tabbar {
position: fixed;
inset-inline: 0;
bottom: 0;
z-index: 45;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
align-items: stretch;
height: var(--mobile-tabbar-h);
min-height: var(--mobile-tabbar-h);
padding: 0.28rem 0.2rem calc(0.28rem + env(safe-area-inset-bottom, 0px));
background: #fff;
border-top: 1px solid #e8eef3;
box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.06);
}
.tab {
appearance: none;
border: 0;
background: transparent;
color: #334155;
display: grid;
justify-items: center;
align-content: center;
gap: 0.12rem;
min-width: 0;
margin: 0.12rem;
padding: 0.28rem 0.1rem;
border-radius: 14px;
font: inherit;
font-size: 0.68rem;
font-weight: 600;
line-height: 1.15;
text-decoration: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition: background 0.15s ease, color 0.15s ease;
}
.tab > span:not(.tab-ico) {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tab:hover {
background: #f8fafc;
color: #0f172a;
}
.tab:active,
.tab.on {
background: #d8faf3;
color: #0f766e;
}
.tab:active :deep(.ico),
.tab.on :deep(.ico) {
color: #0f766e;
}
.tab-ico {
position: relative;
display: grid;
place-items: center;
}
.tab-badge {
position: absolute;
top: -0.35rem;
inset-inline-start: 0.85rem;
min-width: 1rem;
height: 1rem;
padding: 0 0.2rem;
border-radius: 999px;
background: #14b8a6;
color: #fff;
font-size: 0.62rem;
font-weight: 800;
display: grid;
place-items: center;
border: 2px solid #fff;
}
.more-layer {
position: fixed;
inset: 0 0 var(--mobile-tabbar-h) 0;
z-index: 40;
display: flex;
flex-direction: column;
background: #fff;
}
.more-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: calc(0.75rem + env(safe-area-inset-top, 0px)) 1rem 0.85rem;
background: #fff;
border-bottom: 1px solid #e8eef3;
}
.more-head h2 {
margin: 0;
font-size: 1.05rem;
font-weight: 800;
color: #0f172a;
}
.more-body {
flex: 1;
overflow: auto;
display: grid;
align-content: start;
gap: 0.35rem;
padding: 0.45rem 0.55rem 0.5rem;
-webkit-tap-highlight-color: transparent;
}
.more-body .nav-item:active {
background: #d8faf3;
color: #0f766e;
}
.more-logout {
justify-self: start;
margin: 0.15rem 0.85rem 0.85rem;
}
}
</style>
<style>
body.mobile-nav-overlay .qc {
display: none;
}
</style>