Polish desktop sidebar branding and tighten automation rules card.
Move profile and collapse control, show org even for one workspace, and center rules labels in the toolbar-height card.
This commit is contained in:
@@ -31,6 +31,8 @@ const currentName = computed(
|
||||
() => auth.user?.organizationName || orgs.value.find((o) => o.id === auth.user?.organizationId)?.name || 'محیط کاری',
|
||||
);
|
||||
|
||||
const canSwitch = computed(() => orgs.value.length > 1);
|
||||
|
||||
function apiError(e: unknown, fallback: string) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||
@@ -48,6 +50,7 @@ async function loadOrgs() {
|
||||
}
|
||||
|
||||
async function toggle() {
|
||||
if (!canSwitch.value) return;
|
||||
error.value = '';
|
||||
if (open.value) {
|
||||
open.value = false;
|
||||
@@ -134,12 +137,13 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="orgs.length > 1" ref="rootEl" class="org-switch">
|
||||
<div ref="rootEl" class="org-switch">
|
||||
<button
|
||||
class="org-btn"
|
||||
:class="{ compact }"
|
||||
:class="{ compact, static: !canSwitch }"
|
||||
type="button"
|
||||
:title="currentName"
|
||||
:aria-disabled="!canSwitch"
|
||||
@click.stop="toggle"
|
||||
>
|
||||
<UiIcon name="building" :size="18" />
|
||||
@@ -147,7 +151,7 @@ onUnmounted(() => {
|
||||
</button>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="open"
|
||||
v-if="open && canSwitch"
|
||||
ref="menuEl"
|
||||
class="menu"
|
||||
:style="menuStyle"
|
||||
@@ -234,6 +238,14 @@ onUnmounted(() => {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.org-btn.static {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.org-btn.static:hover {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.org-btn.compact {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -251,6 +263,10 @@ onUnmounted(() => {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.org-btn.compact.static:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: fixed;
|
||||
z-index: 90;
|
||||
|
||||
@@ -24,8 +24,8 @@ withDefaults(
|
||||
:style="{ width: `${size}px`, height: `${size}px` }"
|
||||
/>
|
||||
<div v-if="showText && variant === 'full'" class="text">
|
||||
<strong>{{ brand.name }}</strong>
|
||||
<small>by {{ brand.company }}</small>
|
||||
<strong>{{ brand.nameFa }}</strong>
|
||||
<small>{{ brand.name }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+182
-139
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
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';
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
NotificationDto,
|
||||
NotificationsListDto,
|
||||
} from '@mento/shared';
|
||||
import { brand } from '@mento/shared';
|
||||
import { canManageSettings, canViewAllRecords } from '@/lib/roles';
|
||||
|
||||
const auth = useAuthStore();
|
||||
@@ -161,41 +160,8 @@ const openHub = computed<NavHubDef | null>(() => {
|
||||
|
||||
const hubCanBack = computed(() => hubStack.value.length > 0);
|
||||
|
||||
function givenName(fullName: string): string {
|
||||
const parts = fullName.trim().split(/\s+/).filter(Boolean);
|
||||
if (parts.length <= 1) return fullName.trim();
|
||||
return parts.slice(0, -1).join(' ');
|
||||
}
|
||||
|
||||
const profileNameEl = ref<HTMLElement | null>(null);
|
||||
const useShortProfileName = ref(false);
|
||||
const profileFullName = computed(() => auth.user?.fullName?.trim() || '');
|
||||
const profileDisplayName = computed(() => {
|
||||
if (!profileFullName.value) return '';
|
||||
return useShortProfileName.value
|
||||
? givenName(profileFullName.value)
|
||||
: profileFullName.value;
|
||||
});
|
||||
|
||||
async function fitProfileName() {
|
||||
if (collapsed.value) {
|
||||
useShortProfileName.value = false;
|
||||
return;
|
||||
}
|
||||
useShortProfileName.value = false;
|
||||
await nextTick();
|
||||
const el = profileNameEl.value;
|
||||
if (!el) return;
|
||||
if (el.scrollWidth > el.clientWidth + 1) {
|
||||
useShortProfileName.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
let profileNameObserver: ResizeObserver | null = null;
|
||||
|
||||
watch([profileFullName, collapsed], () => {
|
||||
void fitProfileName();
|
||||
});
|
||||
const profileDisplayName = computed(() => profileFullName.value);
|
||||
|
||||
function closeHub() {
|
||||
openHubKey.value = null;
|
||||
@@ -394,14 +360,6 @@ onMounted(() => {
|
||||
void refreshNotifs();
|
||||
void loadActivityBadge();
|
||||
pollTimer = setInterval(() => void refreshNotifs(), 45000);
|
||||
void fitProfileName();
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
profileNameObserver = new ResizeObserver(() => {
|
||||
void fitProfileName();
|
||||
});
|
||||
const meta = profileNameEl.value?.parentElement;
|
||||
if (meta) profileNameObserver.observe(meta);
|
||||
}
|
||||
mobileMq = window.matchMedia('(max-width: 900px)');
|
||||
isMobileNav.value = mobileMq.matches;
|
||||
mobileMq.addEventListener('change', onMobileMqChange);
|
||||
@@ -411,7 +369,6 @@ onUnmounted(() => {
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
document.body.style.overflow = '';
|
||||
document.body.classList.remove('mobile-nav-overlay');
|
||||
profileNameObserver?.disconnect();
|
||||
mobileMq?.removeEventListener('change', onMobileMqChange);
|
||||
});
|
||||
</script>
|
||||
@@ -422,9 +379,9 @@ onUnmounted(() => {
|
||||
<aside id="app-sidebar" class="sidebar" :class="{ collapsed }">
|
||||
<div class="brand-row">
|
||||
<div class="brand-cluster">
|
||||
<MentoLogo class="sidebar-logo" :size="32" />
|
||||
<MentoLogo class="sidebar-logo" :size="52" />
|
||||
<RouterLink
|
||||
class="profile-card"
|
||||
class="profile-card profile-brand"
|
||||
:to="{ name: 'profile' }"
|
||||
:title="auth.user?.fullName || 'پروفایل'"
|
||||
@click="closeMenu"
|
||||
@@ -441,7 +398,7 @@ onUnmounted(() => {
|
||||
<i class="online-dot" title="آنلاین" aria-hidden="true" />
|
||||
</span>
|
||||
<span class="profile-meta">
|
||||
<strong ref="profileNameEl">{{ profileDisplayName }}</strong>
|
||||
<strong>{{ profileDisplayName }}</strong>
|
||||
</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
@@ -453,6 +410,16 @@ onUnmounted(() => {
|
||||
>
|
||||
✕
|
||||
</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">
|
||||
@@ -622,33 +589,42 @@ onUnmounted(() => {
|
||||
</p>
|
||||
|
||||
<div class="sidebar-foot">
|
||||
<div class="promo-card">
|
||||
<span class="promo-icons" aria-hidden="true">
|
||||
<UiIcon name="send" :size="20" />
|
||||
<UiIcon name="sparkle" :size="18" />
|
||||
</span>
|
||||
<p>
|
||||
<span>با {{ brand.nameFa }} فروشات را</span>
|
||||
<span>چند برابر کن</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="foot-actions">
|
||||
<OrgSwitcher :compact="collapsed" />
|
||||
<button class="logout-link" type="button" title="خروج از حساب" @click="onLogout">
|
||||
<UiIcon name="logout" :size="18" />
|
||||
<span>خروج از حساب</span>
|
||||
</button>
|
||||
<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>
|
||||
<button
|
||||
class="collapse-btn desktop-only"
|
||||
type="button"
|
||||
:aria-pressed="collapsed"
|
||||
:aria-label="collapsed ? 'باز کردن منو' : 'جمع کردن منو'"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<UiIcon name="chevron" :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -977,7 +953,7 @@ onUnmounted(() => {
|
||||
padding: 0.35rem 0.85rem 1.1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.35rem;
|
||||
gap: 0.65rem;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.06);
|
||||
@@ -988,7 +964,7 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: inherit;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.brand-row {
|
||||
@@ -1009,7 +985,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.brand-cluster :deep(.brand-logo.full) {
|
||||
gap: 0.35rem;
|
||||
gap: 0.55rem;
|
||||
flex-shrink: 0;
|
||||
min-width: max-content;
|
||||
}
|
||||
@@ -1023,18 +999,19 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.brand-cluster :deep(.text strong) {
|
||||
font-size: 0.92rem;
|
||||
font-size: 1.28rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.brand-cluster :deep(.text small) {
|
||||
font-size: 0.62rem;
|
||||
font-size: 0.82rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.side-nav {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
gap: 0.18rem;
|
||||
align-content: start;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
@@ -1049,19 +1026,20 @@ onUnmounted(() => {
|
||||
.nav-primary,
|
||||
.nav-sub {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
gap: 0.2rem;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.82rem 1rem;
|
||||
border-radius: 18px;
|
||||
gap: 0.65rem;
|
||||
padding: 0.62rem 0.9rem;
|
||||
border-radius: 16px;
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.94rem;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
@@ -1157,13 +1135,14 @@ onUnmounted(() => {
|
||||
|
||||
.nav-sep {
|
||||
height: 1px;
|
||||
margin: 0.55rem 0.85rem;
|
||||
margin: 0.35rem 0.85rem;
|
||||
background: #e8eef4;
|
||||
}
|
||||
|
||||
.nav-utility {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
gap: 0.2rem;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.chev {
|
||||
@@ -1219,6 +1198,11 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.foot-actions {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.foot-meta {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
@@ -1238,6 +1222,10 @@ onUnmounted(() => {
|
||||
max-width: 4.8rem;
|
||||
}
|
||||
|
||||
.profile-foot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-card:hover {
|
||||
background: #f0fdfa;
|
||||
}
|
||||
@@ -1297,42 +1285,6 @@ onUnmounted(() => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.promo-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.05rem 1.05rem;
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(105deg, #c4e84a 0%, #2dd4bf 42%, #1d4ed8 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 12px 26px rgba(29, 78, 216, 0.18);
|
||||
}
|
||||
|
||||
.promo-icons {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.promo-card p {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
display: grid;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.promo-card p span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logout-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1351,21 +1303,35 @@ onUnmounted(() => {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
width: 100%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
color: #64748b;
|
||||
color: #94a3b8;
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
min-height: 36px;
|
||||
padding: 0.35rem 0.5rem;
|
||||
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);
|
||||
}
|
||||
@@ -1425,6 +1391,85 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
@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);
|
||||
@@ -1475,17 +1520,17 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.sidebar.collapsed .brand-cluster :deep(.mark) {
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
width: 34px !important;
|
||||
height: 34px !important;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .brand-cluster :deep(.text strong) {
|
||||
font-size: 0.82rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .brand-cluster :deep(.text small) {
|
||||
font-size: 0.58rem;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -1603,7 +1648,6 @@ onUnmounted(() => {
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .promo-card,
|
||||
.sidebar.collapsed .scope-banner {
|
||||
display: none;
|
||||
}
|
||||
@@ -1635,10 +1679,9 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.sidebar.collapsed .collapse-btn {
|
||||
min-height: 32px;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem;
|
||||
border-radius: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-inline-start: 0.15rem;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .collapse-btn :deep(.ico) {
|
||||
|
||||
@@ -567,14 +567,21 @@ watch(trigger, (next) => {
|
||||
|
||||
.rules {
|
||||
grid-area: rules;
|
||||
/* Let the toolbar set the row height; don't let the rules card grow it. */
|
||||
/* Match toolbar row height; center content vertically in the card. */
|
||||
height: 0;
|
||||
min-height: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
gap: 0.35rem;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.rules .muted {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.studio {
|
||||
|
||||
Reference in New Issue
Block a user