feat : sidebar et navbar responsive mobile
Sidebar en overlay slide-in sous 1024px avec hamburger menu, overlay semi-transparent, et fermeture auto au clic sur un lien. Navbar adaptée avec padding réduit et username masqué sur petit écran. Dropdown notifications responsive (largeur relative au viewport). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<header ref="headerRef" class="border-b border-neutral-200 bg-primary-500 p-5 text-white">
|
<header ref="headerRef" class="border-b border-neutral-200 bg-primary-500 px-4 py-3 text-white lg:p-5">
|
||||||
<div class="flex h-full items-center justify-end">
|
<div class="flex h-full items-center justify-between lg:justify-end">
|
||||||
<div class="flex gap-6 text-xl text-white">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md p-1 text-white hover:text-neutral-200 lg:hidden"
|
||||||
|
@click="$emit('toggleSidebar')"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:menu" size="28"/>
|
||||||
|
</button>
|
||||||
|
<div class="flex gap-4 text-xl text-white lg:gap-6">
|
||||||
<div v-if="isAdmin" ref="bellRoot" class="relative">
|
<div v-if="isAdmin" ref="bellRoot" class="relative">
|
||||||
<button type="button" class="relative self-center cursor-pointer flex items-center" @click="toggleNotifications">
|
<button type="button" class="relative self-center cursor-pointer flex items-center" @click="toggleNotifications">
|
||||||
<Icon name="mdi:bell-plus" size="36"/>
|
<Icon name="mdi:bell-plus" size="36"/>
|
||||||
@@ -15,8 +22,8 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="isNotificationsOpen"
|
v-if="isNotificationsOpen"
|
||||||
class="fixed right-[20px] z-30 w-[400px] rounded-md border border-neutral-200 bg-white text-neutral-800 shadow-lg"
|
class="fixed right-2 z-30 w-[calc(100vw-1rem)] max-w-[400px] rounded-md border border-neutral-200 bg-white text-neutral-800 shadow-lg lg:right-[20px]"
|
||||||
:style="{ top: `${navbarBottom + 20}px` }"
|
:style="{ top: `${navbarBottom + 10}px` }"
|
||||||
>
|
>
|
||||||
<div class="px-3 pt-3 pb-6 text-xl font-semibold">
|
<div class="px-3 pt-3 pb-6 text-xl font-semibold">
|
||||||
Notifications
|
Notifications
|
||||||
@@ -66,7 +73,7 @@
|
|||||||
<div ref="userMenuRoot" class="relative flex gap-4">
|
<div ref="userMenuRoot" class="relative flex gap-4">
|
||||||
<button type="button" class="flex items-center gap-4 cursor-pointer" @click="toggleUserMenu">
|
<button type="button" class="flex items-center gap-4 cursor-pointer" @click="toggleUserMenu">
|
||||||
<Icon name="mdi:account-circle-outline" class="self-center" size="36"/>
|
<Icon name="mdi:account-circle-outline" class="self-center" size="36"/>
|
||||||
<p class="self-center">{{ user?.username }}</p>
|
<p class="hidden self-center sm:block">{{ user?.username }}</p>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="isUserMenuOpen"
|
v-if="isUserMenuOpen"
|
||||||
@@ -103,6 +110,10 @@ defineProps<{
|
|||||||
user?: User
|
user?: User
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(event: 'toggleSidebar'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const formatTimeAgo = (dateString: string): string => {
|
const formatTimeAgo = (dateString: string): string => {
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|||||||
@@ -1,11 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-screen overflow-hidden">
|
<div class="h-screen overflow-hidden">
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<aside class="flex h-full w-64 flex-shrink-0 flex-col border-r border-neutral-200 bg-tertiary-500">
|
<!-- Mobile overlay -->
|
||||||
<div class="h-[75px]">
|
<Transition
|
||||||
|
enter-active-class="transition-opacity duration-300"
|
||||||
|
enter-from-class="opacity-0"
|
||||||
|
enter-to-class="opacity-100"
|
||||||
|
leave-active-class="transition-opacity duration-300"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="sidebarOpen"
|
||||||
|
class="fixed inset-0 z-40 bg-black/50 lg:hidden"
|
||||||
|
@click="sidebarOpen = false"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside
|
||||||
|
:class="[
|
||||||
|
'fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-neutral-200 bg-tertiary-500 transition-transform duration-300 lg:static lg:translate-x-0 lg:flex-shrink-0',
|
||||||
|
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex h-[75px] items-center justify-between">
|
||||||
<img src="/malio.png" alt="Logo" class="w-auto"/>
|
<img src="/malio.png" alt="Logo" class="w-auto"/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mr-3 rounded-md p-1 text-neutral-500 hover:text-primary-500 lg:hidden"
|
||||||
|
@click="sidebarOpen = false"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:close" size="24"/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<nav class="flex-1 px-4 pb-6">
|
<nav class="flex-1 overflow-y-auto px-4 pb-6">
|
||||||
<template v-if="isAdmin">
|
<template v-if="isAdmin">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/calendar"
|
to="/calendar"
|
||||||
@@ -13,6 +42,7 @@
|
|||||||
:class="route.path.startsWith('/calendar')
|
:class="route.path.startsWith('/calendar')
|
||||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||||
: ''"
|
: ''"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:calendar-blank" size="24"/>
|
<Icon name="mdi:calendar-blank" size="24"/>
|
||||||
<p>Calendrier</p>
|
<p>Calendrier</p>
|
||||||
@@ -26,6 +56,7 @@
|
|||||||
route.path.startsWith('/hours') ? 'bg-tertiary-500 text-primary-500 font-bold' : '',
|
route.path.startsWith('/hours') ? 'bg-tertiary-500 text-primary-500 font-bold' : '',
|
||||||
!isAdmin ? 'border-t border-secondary-500 pt-3' : ''
|
!isAdmin ? 'border-t border-secondary-500 pt-3' : ''
|
||||||
]"
|
]"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:clock-time-four-outline" size="24"/>
|
<Icon name="mdi:clock-time-four-outline" size="24"/>
|
||||||
<p>Heures</p>
|
<p>Heures</p>
|
||||||
@@ -38,6 +69,7 @@
|
|||||||
route.path.startsWith('/driver-hours') ? 'bg-tertiary-500 text-primary-500 font-bold' : '',
|
route.path.startsWith('/driver-hours') ? 'bg-tertiary-500 text-primary-500 font-bold' : '',
|
||||||
!isAdmin && isDriver ? 'border-t border-secondary-500 pt-3' : ''
|
!isAdmin && isDriver ? 'border-t border-secondary-500 pt-3' : ''
|
||||||
]"
|
]"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:truck-outline" size="24"/>
|
<Icon name="mdi:truck-outline" size="24"/>
|
||||||
<p>Heures Conducteurs</p>
|
<p>Heures Conducteurs</p>
|
||||||
@@ -49,6 +81,7 @@
|
|||||||
:class="route.path.startsWith('/employees')
|
:class="route.path.startsWith('/employees')
|
||||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||||
: ''"
|
: ''"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:account-group-outline" size="24"/>
|
<Icon name="mdi:account-group-outline" size="24"/>
|
||||||
<p>Employés</p>
|
<p>Employés</p>
|
||||||
@@ -60,6 +93,7 @@
|
|||||||
:class="route.path.startsWith('/leave-recap')
|
:class="route.path.startsWith('/leave-recap')
|
||||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||||
: ''"
|
: ''"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:beach" size="24"/>
|
<Icon name="mdi:beach" size="24"/>
|
||||||
<p>Récap. congés</p>
|
<p>Récap. congés</p>
|
||||||
@@ -70,6 +104,7 @@
|
|||||||
:class="route.path.startsWith('/sites')
|
:class="route.path.startsWith('/sites')
|
||||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||||
: ''"
|
: ''"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:business" size="24"/>
|
<Icon name="mdi:business" size="24"/>
|
||||||
<p>Sites</p>
|
<p>Sites</p>
|
||||||
@@ -80,6 +115,7 @@
|
|||||||
:class="route.path.startsWith('/absence-types')
|
:class="route.path.startsWith('/absence-types')
|
||||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||||
: ''"
|
: ''"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:umbrella-beach-outline" size="24"/>
|
<Icon name="mdi:umbrella-beach-outline" size="24"/>
|
||||||
<p>Types de statut</p>
|
<p>Types de statut</p>
|
||||||
@@ -90,6 +126,7 @@
|
|||||||
:class="route.path.startsWith('/users')
|
:class="route.path.startsWith('/users')
|
||||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||||
: ''"
|
: ''"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:account-outline" size="24"/>
|
<Icon name="mdi:account-outline" size="24"/>
|
||||||
<p>Utilisateurs</p>
|
<p>Utilisateurs</p>
|
||||||
@@ -100,6 +137,7 @@
|
|||||||
to="/leave-recap"
|
to="/leave-recap"
|
||||||
class="flex items-center gap-2 py-3 text-md text-black hover:bg-tertiary-500 hover:text-primary-500 pt-3"
|
class="flex items-center gap-2 py-3 text-md text-black hover:bg-tertiary-500 hover:text-primary-500 pt-3"
|
||||||
:class="route.path.startsWith('/leave-recap') ? 'bg-tertiary-500 text-primary-500 font-bold' : ''"
|
:class="route.path.startsWith('/leave-recap') ? 'bg-tertiary-500 text-primary-500 font-bold' : ''"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:beach" size="24"/>
|
<Icon name="mdi:beach" size="24"/>
|
||||||
<p>Récap. congés</p>
|
<p>Récap. congés</p>
|
||||||
@@ -111,6 +149,7 @@
|
|||||||
:class="route.path.startsWith('/audit-logs')
|
:class="route.path.startsWith('/audit-logs')
|
||||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||||
: ''"
|
: ''"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:clipboard-text-clock-outline" size="24"/>
|
<Icon name="mdi:clipboard-text-clock-outline" size="24"/>
|
||||||
<p>Journal</p>
|
<p>Journal</p>
|
||||||
@@ -121,6 +160,7 @@
|
|||||||
:class="route.path.startsWith('/documentation')
|
:class="route.path.startsWith('/documentation')
|
||||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||||
: ''"
|
: ''"
|
||||||
|
@click="closeSidebarOnMobile"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:book-open-page-variant-outline" size="24"/>
|
<Icon name="mdi:book-open-page-variant-outline" size="24"/>
|
||||||
<p>Documentation</p>
|
<p>Documentation</p>
|
||||||
@@ -132,9 +172,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="h-full flex-1 overflow-hidden flex flex-col">
|
<div class="h-full flex-1 overflow-hidden flex flex-col min-w-0">
|
||||||
<AppTopNav :user="auth.user" />
|
<AppTopNav :user="auth.user" @toggle-sidebar="sidebarOpen = !sidebarOpen" />
|
||||||
<main class="flex-1 overflow-y-auto px-8 py-12">
|
<main class="flex-1 overflow-y-auto px-4 py-6 lg:px-8 lg:py-12">
|
||||||
<slot/>
|
<slot/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,4 +190,9 @@ const isSuperAdmin = computed(() => auth.user?.roles?.includes('ROLE_SUPER_ADMIN
|
|||||||
const isDriver = computed(() => auth.user?.isDriver ?? false)
|
const isDriver = computed(() => auth.user?.isDriver ?? false)
|
||||||
const hasLeaveRecapAccess = computed(() => auth.user?.hasLeaveRecapAccess ?? false)
|
const hasLeaveRecapAccess = computed(() => auth.user?.hasLeaveRecapAccess ?? false)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const sidebarOpen = ref(false)
|
||||||
|
|
||||||
|
const closeSidebarOnMobile = () => {
|
||||||
|
sidebarOpen.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user