From 51684daa614545375fb167ab509c8059ce0d5cc2 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Wed, 9 Sep 2026 17:51:40 +0330 Subject: [PATCH] Let the sidebar collapse to an icon rail with a logo-and-profile cap. Co-authored-by: Cursor --- apps/web/src/components/UiIcon.vue | 8 +- apps/web/src/layouts/AppLayout.vue | 612 +++++++++++++++++++++++++---- 2 files changed, 544 insertions(+), 76 deletions(-) diff --git a/apps/web/src/components/UiIcon.vue b/apps/web/src/components/UiIcon.vue index 787b08c..efc4b8e 100644 --- a/apps/web/src/components/UiIcon.vue +++ b/apps/web/src/components/UiIcon.vue @@ -57,7 +57,8 @@ defineProps<{ | 'apps-plus' | 'crown' | 'gift' - | 'medal'; + | 'medal' + | 'logout'; size?: number; }>(); @@ -340,6 +341,11 @@ defineProps<{ + diff --git a/apps/web/src/layouts/AppLayout.vue b/apps/web/src/layouts/AppLayout.vue index 83653f7..f60a457 100644 --- a/apps/web/src/layouts/AppLayout.vue +++ b/apps/web/src/layouts/AppLayout.vue @@ -14,7 +14,7 @@ import type { NotificationsListDto, } from '@mento/shared'; import { brand } from '@mento/shared'; -import { USER_ROLE_LABELS, canManageSettings, canViewAllRecords } from '@/lib/roles'; +import { canManageSettings, canViewAllRecords } from '@/lib/roles'; const auth = useAuthStore(); const router = useRouter(); @@ -24,11 +24,12 @@ const canSettings = computed(() => canManageSettings(auth.user?.role)); const showOwnRecordsBanner = computed( () => Boolean(auth.user?.role) && !canViewAllRecords(auth.user?.role), ); -const roleLabel = computed(() => - auth.user?.role ? USER_ROLE_LABELS[auth.user.role] : '', -); const todayLabel = computed(() => formatJalaliShort(new Date())); +const SIDEBAR_KEY = 'mento_sidebar_collapsed'; +const collapsed = ref(false); +const moreFlyout = ref(false); + const salesOpen = ref(false); const moreOpen = ref(false); const settingsOpen = ref(false); @@ -58,6 +59,21 @@ const salesActive = computed(() => pathIn(salesPaths)); const moreActive = computed(() => pathIn(morePaths)); const settingsActive = computed(() => pathIn(settingsPaths)); const productsActive = computed(() => route.path.startsWith('/settings/products')); +const extraActive = computed(() => + pathIn([ + '/quotes', + '/invoices', + '/sales/forecast', + '/notes', + '/tickets', + '/settings/automations', + '/settings/lead-forms', + '/settings/custom-fields', + '/settings/currencies', + '/settings/tahesab', + '/settings/faq', + ]) || moreActive.value, +); watch( () => route.path, @@ -88,6 +104,32 @@ const hasUnread = computed(() => unreadCount.value > 0); function closeMenu() { menuOpen.value = false; + moreFlyout.value = false; +} + +function toggleCollapsed() { + collapsed.value = !collapsed.value; + moreFlyout.value = false; + try { + localStorage.setItem(SIDEBAR_KEY, collapsed.value ? '1' : '0'); + } catch { + // ignore + } +} + +const moreBtnRef = ref(null); +const flyoutRef = ref(null); + +function toggleMoreFlyout() { + moreFlyout.value = !moreFlyout.value; +} + +function onDocClick(event: MouseEvent) { + if (!moreFlyout.value) return; + const target = event.target; + if (!(target instanceof Node)) return; + if (moreBtnRef.value?.contains(target) || flyoutRef.value?.contains(target)) return; + moreFlyout.value = false; } function closeNotif() { @@ -189,24 +231,54 @@ watch( ); onMounted(() => { + try { + collapsed.value = localStorage.getItem(SIDEBAR_KEY) === '1'; + } catch { + collapsed.value = false; + } void refreshNotifs(); void loadActivityBadge(); pollTimer = setInterval(() => void refreshNotifs(), 45000); + document.addEventListener('click', onDocClick, true); }); onUnmounted(() => { if (pollTimer) clearInterval(pollTimer); + document.removeEventListener('click', onDocClick, true); document.body.style.overflow = ''; });