Match dashboard header and shell layout to the Mento mockup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 15:39:18 +03:30
co-authored by Cursor
parent 523e641d8f
commit da6c494fcb
4 changed files with 867 additions and 452 deletions
+133
View File
@@ -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
View File
@@ -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;
+6
View File
@@ -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)]!);
}
File diff suppressed because it is too large Load Diff