refactor: align main layout shell

This commit is contained in:
2026-05-04 14:46:13 -04:00
parent 2d472892d6
commit 9bdef978bd
4 changed files with 261 additions and 242 deletions

View File

@@ -1,15 +1,6 @@
<template>
<v-app>
<div class="shell-container">
<app-bar
:show-brand="true"
:collapse-brand="showsAppSidebar && !isSidebarExpanded"
/>
<div
class="shell-main"
:class="{ 'shell-main-app': showsAppSidebar }"
>
<template v-if="showsAppSidebar">
<div class="shell-sidebar-wrap">
<app-sidebar :is-expanded="isSidebarExpanded" />
@@ -24,6 +15,12 @@
</div>
</template>
<div
class="shell-main"
:class="{ 'shell-main-app': showsAppSidebar }"
>
<app-bar v-if="showsAppSidebar" />
<div class="shell-view">
<router-view></router-view>
</div>
@@ -63,7 +60,7 @@
<style scoped>
.shell-container {
@apply min-h-screen flex flex-col;
@apply min-h-screen flex flex-row;
@apply w-full font-sans;
background:
radial-gradient(circle at top left, rgba(255, 174, 94, 0.18), transparent 28%),
@@ -73,19 +70,19 @@
}
.shell-main {
@apply relative flex flex-1 flex-col;
@apply relative flex min-w-0 flex-1 flex-col;
}
.shell-main-app {
@apply md:flex-row md:items-start;
@apply min-h-screen;
}
.shell-sidebar-wrap {
@apply relative flex-shrink-0;
@apply sticky top-0 z-30 h-screen flex-shrink-0;
}
.sidebar-boundary-toggle {
@apply absolute left-full top-8 z-10 flex h-10 w-10 -translate-x-1/2 items-center justify-center rounded-full border transition-colors;
@apply absolute left-full top-8 z-40 flex h-10 w-10 -translate-x-1/2 items-center justify-center rounded-full border transition-colors;
background: rgba(255, 250, 242, 0.98);
border-color: rgba(23, 32, 51, 0.12);
color: #44516a;

View File

@@ -10,17 +10,6 @@
mdiPlus,
} from '@mdi/js';
const props = defineProps({
showBrand: {
type: Boolean,
default: true,
},
collapseBrand: {
type: Boolean,
default: false,
},
});
const route = useRoute();
const { t } = useI18n();
const authStore = useAuthStore();
@@ -73,21 +62,6 @@
<template>
<nav class="side-container">
<div class="brand-block">
<router-link
v-if="showBrand"
class="brand-link"
:class="{ 'brand-link-collapsed': collapseBrand }"
to="/"
>
<span class="brand-mark">S</span>
<div v-if="!collapseBrand">
<div class="brand-name">Socialize</div>
<div class="brand-caption">{{ t('nav.brandCaption') }}</div>
</div>
</router-link>
</div>
<div class="side-menu">
<div class="side-menu-items side-menu-left">
<WorkspaceSelector
@@ -123,44 +97,15 @@
<style scoped>
.side-container {
@apply sticky top-0 z-10 flex flex-col gap-4 px-5 py-4 md:flex-row md:items-center md:justify-between;
@apply sticky top-0 z-20 flex flex-col gap-4 px-5 py-4 md:flex-row md:items-center md:justify-between;
background: rgba(255, 250, 242, 0.82);
backdrop-filter: blur(18px);
border-bottom: 1px solid rgba(23, 32, 51, 0.08);
isolation: isolate;
}
.brand-block {
@apply flex items-center gap-3;
}
.brand-link {
@apply flex items-center gap-3 no-underline;
color: inherit;
}
.brand-link-collapsed {
@apply gap-0;
}
.brand-mark {
@apply flex h-11 w-11 items-center justify-center rounded-2xl text-lg font-black;
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
color: #fffaf2;
}
.brand-name {
@apply text-lg font-black uppercase tracking-[0.18em];
color: #172033;
}
.brand-caption {
@apply text-xs uppercase tracking-[0.24em];
color: #5d6b82;
}
.side-menu {
@apply flex flex-1 items-center justify-between gap-3;
@apply flex w-full flex-1 items-center justify-between gap-3;
}
.side-menu-items {

View File

@@ -2,15 +2,13 @@
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import AppAvatar from '@/components/AppAvatar.vue';
import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { useChannelsStore } from '@/features/channels/stores/channelsStore.js';
import { useLanguageStore } from '@/stores/languageStore.js';
import { useNotificationsStore } from '@/features/notifications/stores/notificationsStore.js';
import { getNotificationRoute } from '@/features/notifications/notificationRoutes.js';
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
import SidebarUserMenu from './SidebarUserMenu.vue';
import {
mdiBellOutline,
mdiCalendarMonthOutline,
@@ -39,16 +37,12 @@
const authStore = useAuthStore();
const channelsStore = useChannelsStore();
const contentItemsStore = useContentItemsStore();
const languageStore = useLanguageStore();
const notificationsStore = useNotificationsStore();
const campaignsStore = useCampaignsStore();
const userProfileStore = useUserProfileStore();
const isUserMenuOpen = ref(false);
const isNotificationsOpen = ref(false);
const isSearchFocused = ref(false);
const searchQuery = ref('');
const userMenuRef = ref(null);
const notificationsRef = ref(null);
const searchRef = ref(null);
@@ -133,25 +127,6 @@
isNotificationsOpen.value = !isNotificationsOpen.value;
}
function toggleUserMenu() {
if (!props.isExpanded) {
return;
}
isUserMenuOpen.value = !isUserMenuOpen.value;
}
function toggleLanguage() {
const nextLocale = languageStore.locale === 'en' ? 'fr' : 'en';
languageStore.setLocale(nextLocale);
isUserMenuOpen.value = false;
}
async function openProfile() {
isUserMenuOpen.value = false;
await router.push({ name: 'settings-user-information' });
}
function formatNotificationTitle(notification) {
return notificationTitleMap.value[notification.eventType] ?? notification.message;
}
@@ -182,11 +157,6 @@
await router.push(result.route);
}
function handleLogout() {
isUserMenuOpen.value = false;
authStore.logout();
}
function handleDocumentClick(event) {
if (searchRef.value && !searchRef.value.contains(event.target)) {
isSearchFocused.value = false;
@@ -195,10 +165,6 @@
if (isNotificationsOpen.value && notificationsRef.value && !notificationsRef.value.contains(event.target)) {
isNotificationsOpen.value = false;
}
if (isUserMenuOpen.value && userMenuRef.value && !userMenuRef.value.contains(event.target)) {
isUserMenuOpen.value = false;
}
}
watch(
@@ -215,15 +181,6 @@
{ immediate: true }
);
watch(
() => props.isExpanded,
isExpanded => {
if (!isExpanded) {
isUserMenuOpen.value = false;
}
}
);
onMounted(() => {
document.addEventListener('click', handleDocumentClick);
});
@@ -238,7 +195,22 @@
class="app-sidebar"
:class="{ 'app-sidebar-collapsed': !isExpanded }"
>
<div class="app-sidebar-inner">
<div class="brand-block">
<router-link
class="brand-link"
:class="{ 'brand-link-collapsed': !isExpanded }"
to="/"
>
<span class="brand-mark">S</span>
<div v-if="isExpanded">
<div class="brand-name">Socialize</div>
<div class="brand-caption">{{ t('nav.brandCaption') }}</div>
</div>
</router-link>
</div>
<div class="app-sidebar-scroll">
<div
v-if="authStore.isAuthenticated"
class="sidebar-section sidebar-utilities"
@@ -578,75 +550,52 @@
</div>
</div>
<div
v-if="authStore.isAuthenticated"
ref="userMenuRef"
class="sidebar-workspace sidebar-workspace-bottom"
>
<button
class="sidebar-workspace-trigger"
type="button"
:title="!isExpanded ? userProfileStore.alias : null"
@click.stop="toggleUserMenu"
>
<AppAvatar
:name="userProfileStore.alias"
:src="userProfileStore.portraitUrl"
size="sm"
/>
<span
v-if="isExpanded"
class="sidebar-workspace-label"
>
{{ userProfileStore.alias }}
</span>
<v-icon
v-if="isExpanded"
:icon="mdiChevronDown"
class="sidebar-workspace-icon"
:class="{ 'sidebar-workspace-icon-open': isUserMenuOpen }"
/>
</button>
</div>
<div
v-if="isExpanded && isUserMenuOpen"
class="sidebar-workspace-menu"
>
<button
class="sidebar-workspace-option"
type="button"
@click="openProfile"
>
{{ t('nav.profile') }}
</button>
<button
class="sidebar-workspace-option"
type="button"
@click="toggleLanguage"
>
{{ t('nav.language') }}
</button>
<button
class="sidebar-workspace-option sidebar-workspace-option-danger"
type="button"
@click="handleLogout"
>
{{ t('nav.signOut') }}
</button>
</div>
</div>
</div>
<SidebarUserMenu
v-if="authStore.isAuthenticated"
:is-expanded="isExpanded"
/>
</aside>
</template>
<style scoped>
.app-sidebar {
@apply w-[19rem] flex-shrink-0 px-4 pb-4 pt-4 transition-[width,padding] duration-200 md:sticky md:top-24 md:h-[calc(100vh-6rem)] md:pt-0;
@apply flex h-full w-[19rem] flex-shrink-0 flex-col px-4 pt-4 transition-[width,padding] duration-200;
border-right: 1px solid rgba(23, 32, 51, 0.08);
}
.app-sidebar-inner {
@apply flex h-full flex-col gap-4 overflow-y-auto py-3 pr-3;
.app-sidebar-scroll {
@apply flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto pb-4 pt-3 pr-3;
}
.brand-block {
@apply flex items-center gap-3;
}
.brand-link {
@apply flex items-center gap-3 no-underline;
color: inherit;
}
.brand-mark {
@apply flex h-11 w-11 items-center justify-center rounded-2xl text-lg font-black;
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
color: #fffaf2;
}
.brand-name {
@apply text-lg font-black uppercase tracking-[0.18em];
color: #172033;
}
.brand-caption {
@apply text-xs uppercase tracking-[0.24em];
color: #5d6b82;
}
.side-menu {
@apply flex flex-1 items-center justify-between gap-3;
}
.sidebar-utilities {
@@ -776,63 +725,6 @@
color: #172033;
}
.sidebar-workspace {
@apply relative flex flex-col gap-2;
}
.sidebar-workspace-bottom {
@apply mt-auto pt-4;
border-top: 1px solid rgba(23, 32, 51, 0.08);
}
.sidebar-workspace-kicker {
@apply px-4 text-[10px] font-bold uppercase tracking-[0.22em];
color: #7a8799;
}
.sidebar-workspace-trigger {
@apply flex w-full items-center gap-3 rounded-[1.1rem] px-4 py-3 text-left transition-colors;
background: rgba(23, 32, 51, 0.04);
color: #172033;
}
.sidebar-workspace-trigger:hover {
background: rgba(23, 32, 51, 0.07);
}
.sidebar-workspace-label {
@apply flex-1 truncate text-sm font-semibold;
}
.sidebar-workspace-icon {
@apply text-base transition-transform;
color: #5d6b82;
}
.sidebar-workspace-icon-open {
transform: rotate(180deg);
}
.sidebar-workspace-menu {
@apply absolute bottom-[calc(100%+0.5rem)] left-0 right-0 z-30 flex flex-col gap-1 rounded-[1.25rem] border p-2;
background: rgba(255, 255, 255, 0.98);
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.12);
}
.sidebar-workspace-option {
@apply rounded-[0.95rem] px-4 py-3 text-left text-sm font-semibold transition-colors;
color: #172033;
}
.sidebar-workspace-option:hover {
background: rgba(23, 32, 51, 0.05);
}
.sidebar-workspace-option-danger {
color: #b91c1c;
}
.sidebar-section {
@apply flex flex-col gap-2;
}

View File

@@ -0,0 +1,185 @@
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import AppAvatar from '@/components/AppAvatar.vue';
import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { useLanguageStore } from '@/stores/languageStore.js';
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
import { mdiChevronDown } from '@mdi/js';
const props = defineProps({
isExpanded: {
type: Boolean,
default: true,
},
});
const router = useRouter();
const { t } = useI18n();
const authStore = useAuthStore();
const languageStore = useLanguageStore();
const userProfileStore = useUserProfileStore();
const isUserMenuOpen = ref(false);
const userMenuRef = ref(null);
function toggleUserMenu() {
if (!props.isExpanded) {
return;
}
isUserMenuOpen.value = !isUserMenuOpen.value;
}
function toggleLanguage() {
const nextLocale = languageStore.locale === 'en' ? 'fr' : 'en';
languageStore.setLocale(nextLocale);
isUserMenuOpen.value = false;
}
async function openProfile() {
isUserMenuOpen.value = false;
await router.push({ name: 'settings-user-information' });
}
function handleLogout() {
isUserMenuOpen.value = false;
authStore.logout();
}
function handleDocumentClick(event) {
if (isUserMenuOpen.value && userMenuRef.value && !userMenuRef.value.contains(event.target)) {
isUserMenuOpen.value = false;
}
}
watch(
() => props.isExpanded,
isExpanded => {
if (!isExpanded) {
isUserMenuOpen.value = false;
}
}
);
onMounted(() => {
document.addEventListener('click', handleDocumentClick);
});
onBeforeUnmount(() => {
document.removeEventListener('click', handleDocumentClick);
});
</script>
<template>
<div
ref="userMenuRef"
class="sidebar-workspace sidebar-workspace-bottom"
>
<button
class="sidebar-workspace-trigger"
type="button"
:title="!isExpanded ? userProfileStore.alias : null"
@click.stop="toggleUserMenu"
>
<AppAvatar
:name="userProfileStore.alias"
:src="userProfileStore.portraitUrl"
size="sm"
/>
<span
v-if="isExpanded"
class="sidebar-workspace-label"
>
{{ userProfileStore.alias }}
</span>
<v-icon
v-if="isExpanded"
:icon="mdiChevronDown"
class="sidebar-workspace-icon"
:class="{ 'sidebar-workspace-icon-open': isUserMenuOpen }"
/>
</button>
<div
v-if="isExpanded && isUserMenuOpen"
class="sidebar-workspace-menu"
>
<button
class="sidebar-workspace-option"
type="button"
@click="openProfile"
>
{{ t('nav.profile') }}
</button>
<button
class="sidebar-workspace-option"
type="button"
@click="toggleLanguage"
>
{{ t('nav.language') }}
</button>
<button
class="sidebar-workspace-option sidebar-workspace-option-danger"
type="button"
@click="handleLogout"
>
{{ t('nav.signOut') }}
</button>
</div>
</div>
</template>
<style scoped>
.sidebar-workspace {
@apply relative flex flex-col gap-2;
}
.sidebar-workspace-bottom {
@apply py-4;
border-top: 1px solid rgba(23, 32, 51, 0.08);
}
.sidebar-workspace-trigger {
@apply flex w-full items-center gap-3 rounded-[1.1rem] px-4 py-3 text-left transition-colors;
background: rgba(23, 32, 51, 0.04);
color: #172033;
}
.sidebar-workspace-trigger:hover {
background: rgba(23, 32, 51, 0.07);
}
.sidebar-workspace-label {
@apply flex-1 truncate text-sm font-semibold;
}
.sidebar-workspace-icon {
@apply text-base transition-transform;
color: #5d6b82;
}
.sidebar-workspace-icon-open {
transform: rotate(180deg);
}
.sidebar-workspace-menu {
@apply absolute bottom-[calc(100%+0.5rem)] left-0 right-0 z-30 flex flex-col gap-1 rounded-[1.25rem] border p-2;
background: rgba(255, 255, 255, 0.98);
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.12);
}
.sidebar-workspace-option {
@apply rounded-[0.95rem] px-4 py-3 text-left text-sm font-semibold transition-colors;
color: #172033;
}
.sidebar-workspace-option:hover {
background: rgba(23, 32, 51, 0.05);
}
.sidebar-workspace-option-danger {
color: #b91c1c;
}
</style>