Match dashboard header and shell layout to the Mento mockup.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
name:
|
||||
| 'home'
|
||||
| 'users'
|
||||
| 'briefcase'
|
||||
| 'check'
|
||||
| 'box'
|
||||
| 'chart'
|
||||
| 'settings'
|
||||
| 'search'
|
||||
| 'sparkle'
|
||||
| 'bell'
|
||||
| 'calendar'
|
||||
| 'mail'
|
||||
| 'megaphone'
|
||||
| 'send'
|
||||
| 'fire'
|
||||
| 'warning'
|
||||
| 'trend'
|
||||
| 'phone'
|
||||
| 'trophy';
|
||||
size?: number;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
class="ico"
|
||||
:width="size ?? 20"
|
||||
:height="size ?? 20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<template v-if="name === 'home'">
|
||||
<path d="M3 10.5 12 3l9 7.5" />
|
||||
<path d="M5 9.5V21h14V9.5" />
|
||||
<path d="M10 21v-6h4v6" />
|
||||
</template>
|
||||
<template v-else-if="name === 'users'">
|
||||
<circle cx="9" cy="8" r="3.2" />
|
||||
<path d="M2.5 19c.8-3 2.9-4.5 6.5-4.5s5.7 1.5 6.5 4.5" />
|
||||
<circle cx="17" cy="9" r="2.4" />
|
||||
<path d="M15.2 14.2c2.3.3 3.9 1.5 4.8 3.8" />
|
||||
</template>
|
||||
<template v-else-if="name === 'briefcase'">
|
||||
<rect x="3" y="7" width="18" height="13" rx="2" />
|
||||
<path d="M8 7V5.5A1.5 1.5 0 0 1 9.5 4h5A1.5 1.5 0 0 1 16 5.5V7" />
|
||||
<path d="M3 12h18" />
|
||||
</template>
|
||||
<template v-else-if="name === 'check'">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="m8.5 12.5 2.4 2.4 4.6-5" />
|
||||
</template>
|
||||
<template v-else-if="name === 'box'">
|
||||
<path d="M12 3 3.5 7.5v9L12 21l8.5-4.5v-9L12 3Z" />
|
||||
<path d="M12 12 3.5 7.5M12 12l8.5-4.5M12 12v9" />
|
||||
</template>
|
||||
<template v-else-if="name === 'chart'">
|
||||
<path d="M4 19h16" />
|
||||
<path d="M7 16V10" />
|
||||
<path d="M12 16V6" />
|
||||
<path d="M17 16v-4" />
|
||||
</template>
|
||||
<template v-else-if="name === 'settings'">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9c.3.6.9 1 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z" />
|
||||
</template>
|
||||
<template v-else-if="name === 'search'">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<path d="m20 20-3.5-3.5" />
|
||||
</template>
|
||||
<template v-else-if="name === 'sparkle'">
|
||||
<path d="M12 3v4M12 17v4M3 12h4M17 12h4" />
|
||||
<path d="m6.5 6.5 2.5 2.5M15 15l2.5 2.5M17.5 6.5 15 9M9 15l-2.5 2.5" />
|
||||
<circle cx="12" cy="12" r="2.2" />
|
||||
</template>
|
||||
<template v-else-if="name === 'bell'">
|
||||
<path d="M6 9a6 6 0 1 1 12 0c0 4 1.5 5.5 1.5 5.5H4.5S6 13 6 9Z" />
|
||||
<path d="M10 19a2 2 0 0 0 4 0" />
|
||||
</template>
|
||||
<template v-else-if="name === 'calendar'">
|
||||
<rect x="3.5" y="5" width="17" height="15" rx="2" />
|
||||
<path d="M8 3.5V7M16 3.5V7M3.5 10h17" />
|
||||
</template>
|
||||
<template v-else-if="name === 'mail'">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||
<path d="m4 7 8 6 8-6" />
|
||||
</template>
|
||||
<template v-else-if="name === 'megaphone'">
|
||||
<path d="M3 11v2a2 2 0 0 0 2 2h1l4 4V5L6 9H5a2 2 0 0 0-2 2Z" />
|
||||
<path d="M14 8.5c2 .8 3.5 2.3 3.5 3.5s-1.5 2.7-3.5 3.5" />
|
||||
<path d="M10 15v3a2 2 0 0 1-2 2" />
|
||||
</template>
|
||||
<template v-else-if="name === 'send'">
|
||||
<path d="m4 12 16-8-6 16-2.5-6.5L4 12Z" />
|
||||
</template>
|
||||
<template v-else-if="name === 'fire'">
|
||||
<path d="M12 3c2 3-1 4 1 7 1.5 2.2 4 2.5 4 6a5 5 0 1 1-10 0c0-3.5 2-5 3-7 1-2-1-4 2-6Z" />
|
||||
</template>
|
||||
<template v-else-if="name === 'warning'">
|
||||
<path d="m12 4 9 16H3L12 4Z" />
|
||||
<path d="M12 10v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</template>
|
||||
<template v-else-if="name === 'trend'">
|
||||
<path d="M4 17 10 11l4 4 6-8" />
|
||||
<path d="M15 7h5v5" />
|
||||
</template>
|
||||
<template v-else-if="name === 'phone'">
|
||||
<path d="M8 3.5h2.5l1 4-2 1.2a12 12 0 0 0 5.8 5.8L16.5 12.5l4 1V16a2 2 0 0 1-2.2 2A16 16 0 0 1 4 6.2 2 2 0 0 1 6 4Z" />
|
||||
</template>
|
||||
<template v-else-if="name === 'trophy'">
|
||||
<path d="M8 21h8" />
|
||||
<path d="M12 17v4" />
|
||||
<path d="M7 4h10v4a5 5 0 0 1-10 0V4Z" />
|
||||
<path d="M7 6H4.5A2.5 2.5 0 0 0 7 8.5" />
|
||||
<path d="M17 6h2.5A2.5 2.5 0 0 1 17 8.5" />
|
||||
</template>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ico {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
+302
-248
@@ -3,7 +3,8 @@ 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 { formatJalaliLong } from '@/lib/jalali';
|
||||
import { formatJalaliShort, toFaDigits } from '@/lib/jalali';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import type {
|
||||
ActivitySummaryDto,
|
||||
NotificationDto,
|
||||
@@ -30,7 +31,7 @@ const userInitials = computed(() => {
|
||||
.map((p) => p[0])
|
||||
.join('');
|
||||
});
|
||||
const todayLabel = computed(() => formatJalaliLong(new Date()));
|
||||
const todayLabel = computed(() => formatJalaliShort(new Date()));
|
||||
|
||||
const salesOpen = ref(false);
|
||||
const moreOpen = ref(false);
|
||||
@@ -38,7 +39,7 @@ const settingsOpen = ref(false);
|
||||
const globalSearch = ref('');
|
||||
const activityBadge = ref(0);
|
||||
|
||||
const salesPaths = ['/quotes', '/invoices', '/sales/forecast', '/reports'];
|
||||
const salesPaths = ['/quotes', '/invoices', '/sales/forecast', '/reports', '/notes', '/tickets'];
|
||||
const morePaths = ['/activities/calendar', '/projects', '/project-cards', '/loyalty'];
|
||||
const settingsPaths = [
|
||||
'/settings/team',
|
||||
@@ -59,6 +60,7 @@ function pathIn(prefixes: string[]) {
|
||||
const salesActive = computed(() => pathIn(salesPaths));
|
||||
const moreActive = computed(() => pathIn(morePaths));
|
||||
const settingsActive = computed(() => pathIn(settingsPaths));
|
||||
const productsActive = computed(() => route.path.startsWith('/settings/products'));
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
@@ -135,9 +137,7 @@ async function markRead(item: NotificationDto) {
|
||||
}
|
||||
closeNotif();
|
||||
closeMenu();
|
||||
if (item.link) {
|
||||
await router.push(item.link);
|
||||
}
|
||||
if (item.link) await router.push(item.link);
|
||||
}
|
||||
|
||||
async function markAll() {
|
||||
@@ -180,11 +180,9 @@ function submitSearch() {
|
||||
watch(menuOpen, (open) => {
|
||||
document.body.style.overflow = open || notifOpen.value ? 'hidden' : '';
|
||||
});
|
||||
|
||||
watch(notifOpen, (open) => {
|
||||
document.body.style.overflow = open || menuOpen.value ? 'hidden' : '';
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
@@ -196,9 +194,7 @@ watch(
|
||||
onMounted(() => {
|
||||
void refreshNotifs();
|
||||
void loadActivityBadge();
|
||||
pollTimer = setInterval(() => {
|
||||
void refreshNotifs();
|
||||
}, 45000);
|
||||
pollTimer = setInterval(() => void refreshNotifs(), 45000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -209,33 +205,23 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="shell" :class="{ 'menu-open': menuOpen }">
|
||||
<div
|
||||
v-if="menuOpen"
|
||||
class="backdrop"
|
||||
aria-hidden="true"
|
||||
@click="closeMenu"
|
||||
/>
|
||||
<div v-if="menuOpen" class="backdrop" aria-hidden="true" @click="closeMenu" />
|
||||
|
||||
<aside id="app-sidebar" class="sidebar" :class="{ open: menuOpen }">
|
||||
<div class="brand-row desktop-only">
|
||||
<div class="brand-row">
|
||||
<div class="brand">
|
||||
<span class="mark">M</span>
|
||||
<div>
|
||||
<strong>منتو</strong>
|
||||
<strong>Mento</strong>
|
||||
<small>by Mentech</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-head mobile-only">
|
||||
<div class="brand">
|
||||
<span class="mark">M</span>
|
||||
<div>
|
||||
<strong>منتو</strong>
|
||||
<small>by Mentech</small>
|
||||
</div>
|
||||
</div>
|
||||
<button class="icon-btn ghost" type="button" aria-label="بستن منو" @click="closeMenu">
|
||||
<button
|
||||
class="icon-btn ghost mobile-only"
|
||||
type="button"
|
||||
aria-label="بستن منو"
|
||||
@click="closeMenu"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
@@ -243,15 +229,15 @@ onUnmounted(() => {
|
||||
<nav class="side-nav">
|
||||
<div class="nav-primary">
|
||||
<RouterLink to="/" class="nav-item" @click="closeMenu">
|
||||
<span class="nav-icon" aria-hidden="true">⌂</span>
|
||||
<UiIcon name="home" :size="18" />
|
||||
<span>داشبورد</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/contacts" class="nav-item" @click="closeMenu">
|
||||
<span class="nav-icon" aria-hidden="true">👥</span>
|
||||
<UiIcon name="users" :size="18" />
|
||||
<span>مشتریان</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/pipeline" class="nav-item" @click="closeMenu">
|
||||
<span class="nav-icon" aria-hidden="true">💼</span>
|
||||
<UiIcon name="briefcase" :size="18" />
|
||||
<span>معاملات</span>
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
@@ -268,18 +254,33 @@ onUnmounted(() => {
|
||||
}"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<span class="nav-icon" aria-hidden="true">☑</span>
|
||||
<UiIcon name="check" :size="18" />
|
||||
<span>فعالیتها</span>
|
||||
<span v-if="activityBadge" class="nav-badge">{{ activityBadge }}</span>
|
||||
<span v-if="activityBadge" class="nav-badge">{{ toFaDigits(activityBadge) }}</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/settings/products" class="nav-item" @click="closeMenu">
|
||||
<span class="nav-icon" aria-hidden="true">📦</span>
|
||||
<RouterLink
|
||||
to="/settings/products"
|
||||
class="nav-item"
|
||||
:class="{ 'router-link-active': productsActive }"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<UiIcon name="box" :size="18" />
|
||||
<span>محصولات</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/reports" class="nav-item" @click="closeMenu">
|
||||
<span class="nav-icon" aria-hidden="true">📊</span>
|
||||
<UiIcon name="chart" :size="18" />
|
||||
<span>گزارشها</span>
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-if="canSettings"
|
||||
to="/settings/team"
|
||||
class="nav-item"
|
||||
:class="{ 'router-link-active': settingsActive && !productsActive }"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<UiIcon name="settings" :size="18" />
|
||||
<span>تنظیمات</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-group" :class="{ open: salesOpen, active: salesActive }">
|
||||
@@ -329,7 +330,7 @@ onUnmounted(() => {
|
||||
:aria-expanded="settingsOpen"
|
||||
@click="toggleGroup('settings', $event)"
|
||||
>
|
||||
<span>تنظیمات</span>
|
||||
<span>جزئیات تنظیمات</span>
|
||||
<span class="chev" aria-hidden="true" />
|
||||
</button>
|
||||
<div v-show="settingsOpen" class="nav-sub">
|
||||
@@ -348,11 +349,6 @@ onUnmounted(() => {
|
||||
</p>
|
||||
|
||||
<div class="sidebar-foot">
|
||||
<div class="promo-card">
|
||||
<span class="promo-icon" aria-hidden="true">🚀</span>
|
||||
<p>با منتو فروشات رو چند برابر کن</p>
|
||||
</div>
|
||||
|
||||
<div class="profile-card">
|
||||
<span class="avatar">{{ userInitials }}</span>
|
||||
<div class="profile-meta">
|
||||
@@ -362,14 +358,19 @@ onUnmounted(() => {
|
||||
<span class="online-dot" title="آنلاین" />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-ghost logout" type="button" @click="onLogout">خروج</button>
|
||||
<div class="promo-card">
|
||||
<span class="promo-icon"><UiIcon name="megaphone" :size="18" /></span>
|
||||
<p>با منتو فروشات رو چند برابر کن</p>
|
||||
</div>
|
||||
|
||||
<button class="logout-link" type="button" @click="onLogout">خروج از حساب</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="workspace">
|
||||
<header class="topbar mobile-only">
|
||||
<header class="mobile-topbar mobile-only">
|
||||
<button
|
||||
class="icon-btn"
|
||||
class="round-btn"
|
||||
type="button"
|
||||
:aria-expanded="menuOpen"
|
||||
aria-controls="app-sidebar"
|
||||
@@ -379,34 +380,44 @@ onUnmounted(() => {
|
||||
<span class="burger" :class="{ open: menuOpen }" />
|
||||
</button>
|
||||
<div class="top-brand">
|
||||
<span class="mark">M</span>
|
||||
<strong>منتو</strong>
|
||||
<span class="mark sm">M</span>
|
||||
<strong>Mento</strong>
|
||||
</div>
|
||||
<button class="icon-btn notif-top" type="button" aria-label="اعلانها" @click="toggleNotif">
|
||||
🔔
|
||||
<span v-if="hasUnread" class="badge">{{ unreadCount }}</span>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<header class="desktop-topbar desktop-only">
|
||||
<form class="global-search" @submit.prevent="submitSearch">
|
||||
<span class="search-icon" aria-hidden="true">🔍</span>
|
||||
<UiIcon name="search" :size="18" class="search-leading" />
|
||||
<input
|
||||
v-model="globalSearch"
|
||||
type="search"
|
||||
placeholder="جستجو در مشتریان، معاملات و…"
|
||||
placeholder="جستجو در مشتریان، معاملات و..."
|
||||
/>
|
||||
<kbd>⌘K</kbd>
|
||||
</form>
|
||||
|
||||
<div class="topbar-actions">
|
||||
<RouterLink class="top-icon" :to="{ name: 'notes' }" title="هوش مصنوعی">✨</RouterLink>
|
||||
<button class="top-icon" type="button" aria-label="اعلانها" @click="toggleNotif">
|
||||
🔔
|
||||
<span v-if="hasUnread" class="badge">{{ unreadCount }}</span>
|
||||
<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="top-icon" :to="{ name: 'activities-calendar' }" title="تقویم">📅</RouterLink>
|
||||
<RouterLink class="top-icon" :to="{ name: 'tickets' }" title="پیامها">✉</RouterLink>
|
||||
<span class="date-chip">{{ todayLabel }}</span>
|
||||
<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 class="date-chip">
|
||||
<UiIcon name="calendar" :size="16" />
|
||||
<span>{{ todayLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -419,7 +430,7 @@ onUnmounted(() => {
|
||||
<button v-if="hasUnread" class="linkish" type="button" @click="markAll">
|
||||
همه خوانده شد
|
||||
</button>
|
||||
<button class="icon-btn ghost" type="button" aria-label="بستن" @click="closeNotif">
|
||||
<button class="round-btn sm" type="button" aria-label="بستن" @click="closeNotif">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
@@ -448,10 +459,10 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
grid-template-columns: 248px minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
background: var(--bg);
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
@@ -460,64 +471,77 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.backdrop,
|
||||
.mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: start;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
padding: 1.25rem 1rem;
|
||||
border-inline-start: 1px solid #e8eef3;
|
||||
padding: 1.15rem 0.9rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border-inline-start: 1px solid var(--line);
|
||||
box-shadow: -4px 0 24px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.brand-row,
|
||||
.drawer-head {
|
||||
.brand-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0 0.35rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.mark {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: linear-gradient(135deg, #2eb29b, #0d9488);
|
||||
background: linear-gradient(145deg, #2ec4a8, #14b8a6);
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
font-size: 1.05rem;
|
||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.28);
|
||||
}
|
||||
|
||||
.mark.sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.brand strong {
|
||||
display: block;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.brand small {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.72rem;
|
||||
margin-top: 0.05rem;
|
||||
}
|
||||
|
||||
.side-nav {
|
||||
@@ -536,32 +560,31 @@ onUnmounted(() => {
|
||||
.nav-primary,
|
||||
.nav-sub {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
gap: 0.22rem;
|
||||
}
|
||||
|
||||
.nav-item,
|
||||
.side-nav a {
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.72rem 0.85rem;
|
||||
gap: 0.7rem;
|
||||
padding: 0.78rem 0.9rem;
|
||||
border-radius: 14px;
|
||||
color: #475569;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
font-size: 0.92rem;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 1.25rem;
|
||||
text-align: center;
|
||||
opacity: 0.85;
|
||||
.nav-item:hover {
|
||||
background: #f8fafc;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.nav-item.router-link-active,
|
||||
.side-nav a.router-link-active {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
.nav-item.router-link-active {
|
||||
background: #14b8a6;
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 22px rgba(20, 184, 166, 0.28);
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
@@ -569,17 +592,19 @@ onUnmounted(() => {
|
||||
min-width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
border-radius: 999px;
|
||||
background: var(--accent);
|
||||
background: #14b8a6;
|
||||
color: #fff;
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.7rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0 0.35rem;
|
||||
padding: 0 0.3rem;
|
||||
}
|
||||
|
||||
.nav-item.router-link-active .nav-badge {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
@@ -588,29 +613,23 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
color: #94a3b8;
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
padding: 0.55rem 0.85rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.nav-group-btn:hover {
|
||||
background: #f8fafc;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.chev {
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
border-right: 1.5px solid currentColor;
|
||||
border-bottom: 1.5px solid currentColor;
|
||||
width: 0.42rem;
|
||||
height: 0.42rem;
|
||||
border-right: 1.6px solid currentColor;
|
||||
border-bottom: 1.6px solid currentColor;
|
||||
transform: rotate(45deg);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
@@ -620,127 +639,155 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.nav-sub {
|
||||
padding: 0.1rem 0 0.25rem 0.35rem;
|
||||
border-inline-end: 1px solid var(--line);
|
||||
margin-inline-end: 0.85rem;
|
||||
margin-inline-end: 0.75rem;
|
||||
padding-inline-end: 0.35rem;
|
||||
border-inline-end: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.nav-sub a {
|
||||
padding: 0.55rem 0.75rem;
|
||||
font-size: 0.92rem;
|
||||
display: block;
|
||||
padding: 0.55rem 0.8rem;
|
||||
border-radius: 10px;
|
||||
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: 10px;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar-foot {
|
||||
margin-top: auto;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
gap: 0.7rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.promo-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #0f766e, #134e4a);
|
||||
color: #ecfdf5;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.promo-card p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--line);
|
||||
padding: 0.55rem 0.65rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
background: linear-gradient(145deg, #99f6e4, #5eead4);
|
||||
color: #0f766e;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-meta {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 0.1rem;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
.profile-meta strong {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.88rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-meta span {
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.online-dot {
|
||||
margin-inline-start: auto;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
|
||||
}
|
||||
|
||||
.promo-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
padding: 0.95rem 1rem;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(145deg, #0f766e, #115e59 55%, #134e4a);
|
||||
color: #ecfdf5;
|
||||
box-shadow: 0 14px 28px rgba(15, 118, 110, 0.25);
|
||||
}
|
||||
|
||||
.promo-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logout {
|
||||
width: 100%;
|
||||
.promo-card p {
|
||||
margin: 0;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.logout-link {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
font: inherit;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
padding: 0 0.35rem;
|
||||
}
|
||||
|
||||
.desktop-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem 0.75rem;
|
||||
padding: 1.05rem 1.5rem 0.85rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: rgba(244, 247, 248, 0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
background: rgba(245, 247, 250, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.global-search {
|
||||
flex: 1;
|
||||
max-width: 520px;
|
||||
max-width: 560px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
gap: 0.7rem;
|
||||
background: #eef2f6;
|
||||
border-radius: 999px;
|
||||
padding: 0.55rem 1rem;
|
||||
box-shadow: var(--shadow);
|
||||
padding: 0.72rem 1rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.global-search:focus-within {
|
||||
background: #fff;
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.global-search input {
|
||||
@@ -748,56 +795,87 @@ onUnmounted(() => {
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
font-size: 0.92rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
opacity: 0.55;
|
||||
.global-search input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.search-leading {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.global-search kbd {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.72rem;
|
||||
color: #94a3b8;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 0.18rem 0.45rem;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.top-icon {
|
||||
.round-btn {
|
||||
position: relative;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
color: #64748b;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
font-size: 1rem;
|
||||
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;
|
||||
}
|
||||
|
||||
.date-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.55rem 0.85rem;
|
||||
border-radius: 12px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
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: -4px;
|
||||
inset-inline-start: -4px;
|
||||
min-width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
top: -2px;
|
||||
inset-inline-start: -2px;
|
||||
min-width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
border-radius: 999px;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
font-size: 0.68rem;
|
||||
font-size: 0.65rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0 0.3rem;
|
||||
padding: 0 0.22rem;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
.notif-layer {
|
||||
@@ -813,7 +891,7 @@ onUnmounted(() => {
|
||||
.notif-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.25);
|
||||
background: rgba(15, 23, 42, 0.22);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -823,8 +901,7 @@ onUnmounted(() => {
|
||||
max-height: min(70vh, 480px);
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--line);
|
||||
border: 1px solid #e8eef3;
|
||||
border-radius: 18px;
|
||||
padding: 0.85rem;
|
||||
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.12);
|
||||
@@ -847,7 +924,7 @@ onUnmounted(() => {
|
||||
.linkish {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
color: #14b8a6;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
@@ -875,7 +952,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.notif-list li.unread button {
|
||||
background: var(--accent-soft);
|
||||
background: #ecfdf8;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -886,7 +963,7 @@ onUnmounted(() => {
|
||||
.body,
|
||||
.time,
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@@ -896,24 +973,8 @@ onUnmounted(() => {
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
min-width: 42px;
|
||||
height: 42px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-btn.ghost {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.burger {
|
||||
width: 18px;
|
||||
width: 16px;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
position: relative;
|
||||
@@ -930,11 +991,11 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.burger::before {
|
||||
top: -6px;
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.burger::after {
|
||||
top: 6px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.burger.open {
|
||||
@@ -942,11 +1003,29 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.burger.open::before {
|
||||
transform: translateY(6px) rotate(45deg);
|
||||
transform: translateY(5px) rotate(45deg);
|
||||
}
|
||||
|
||||
.burger.open::after {
|
||||
transform: translateY(-6px) rotate(-45deg);
|
||||
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;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@@ -954,25 +1033,6 @@ onUnmounted(() => {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.7rem 0.85rem;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--line);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.top-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -981,12 +1041,6 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.drawer-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
|
||||
@@ -170,6 +170,12 @@ export function formatJalaliLong(d: Date): string {
|
||||
return `${weekdayName(d)} ${toFaDigits(jd)} ${jalaliMonthName(jm)} ${toFaDigits(jy)}`;
|
||||
}
|
||||
|
||||
/** مثل: دوشنبه ۲۶ مرداد */
|
||||
export function formatJalaliShort(d: Date): string {
|
||||
const { jm, jd } = dateToJalali(d);
|
||||
return `${weekdayName(d)} ${toFaDigits(jd)} ${jalaliMonthName(jm)}`;
|
||||
}
|
||||
|
||||
export function toFaDigits(value: number | string): string {
|
||||
return String(value).replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[Number(d)]!);
|
||||
}
|
||||
|
||||
+426
-204
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user