refactor(layout): extract AppNavbar component and rewrite app.vue (F7.3)
Extract 680-line navbar into LayoutAppNavbar component with useNavDropdown composable. app.vue reduced from 698 to 22 LOC. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
855
app/app.vue
855
app/app.vue
@@ -1,698 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
<!-- Navbar -->
|
<LayoutAppNavbar
|
||||||
<div class="navbar bg-base-100 shadow-lg">
|
@open-settings="displaySettingsOpen = true"
|
||||||
<div class="navbar-start">
|
@logout="handleLogout"
|
||||||
<div class="dropdown">
|
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
|
||||||
<IconLucideMenu class="w-5 h-5" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
tabindex="0"
|
|
||||||
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
|
|
||||||
>
|
|
||||||
<li class="pt-1 pb-2 lg:hidden">
|
|
||||||
<button
|
|
||||||
class="w-full flex items-center gap-2 rounded-md px-2 py-1 transition-colors text-base-content hover:bg-primary/10 hover:text-primary"
|
|
||||||
@click="openDisplaySettings"
|
|
||||||
>
|
|
||||||
<IconLucideSettings class="w-4 h-4" aria-hidden="true" />
|
|
||||||
Paramètres d'affichage
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Vue d'ensemble
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/machines"
|
|
||||||
class="rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/machines')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Parc Machines
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/machine-skeleton"
|
|
||||||
class="rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/machine-skeleton')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Squelettes de machine
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li class="mt-1 border-t border-base-200 pt-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex w-full items-center justify-between rounded-md px-2 py-1 text-left transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/piece-category') || isActive('/pieces-catalog')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
@click="toggleDropdown('pieces-mobile')"
|
|
||||||
@keydown.enter.prevent="toggleDropdown('pieces-mobile')"
|
|
||||||
@keydown.space.prevent="toggleDropdown('pieces-mobile')"
|
|
||||||
:aria-expanded="openDropdown === 'pieces-mobile'"
|
|
||||||
>
|
|
||||||
<span>Pièces</span>
|
|
||||||
<IconLucideChevronRight
|
|
||||||
class="h-4 w-4 transition-transform"
|
|
||||||
:class="openDropdown === 'pieces-mobile' ? 'rotate-90' : ''"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
/>
|
||||||
</button>
|
|
||||||
<Transition name="nav-dropdown-mobile">
|
|
||||||
<ul
|
|
||||||
v-if="openDropdown === 'pieces-mobile'"
|
|
||||||
class="mt-2 space-y-1 rounded-md border border-base-200 bg-base-100 p-2 shadow-sm overflow-hidden"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/pieces-catalog"
|
|
||||||
class="rounded-md px-2 py-1 transition-colors block"
|
|
||||||
:class="
|
|
||||||
isActive('/pieces-catalog')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catalogue des pièces
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/piece-category"
|
|
||||||
class="rounded-md px-2 py-1 transition-colors block"
|
|
||||||
:class="
|
|
||||||
isActive('/piece-category')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catégorie de pièce
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Transition>
|
|
||||||
</li>
|
|
||||||
<li class="mt-1 border-t border-base-200 pt-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex w-full items-center justify-between rounded-md px-2 py-1 text-left transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/product-category') || isActive('/product-catalog')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
@click="toggleDropdown('products-mobile')"
|
|
||||||
@keydown.enter.prevent="toggleDropdown('products-mobile')"
|
|
||||||
@keydown.space.prevent="toggleDropdown('products-mobile')"
|
|
||||||
:aria-expanded="openDropdown === 'products-mobile'"
|
|
||||||
>
|
|
||||||
<span>Produits</span>
|
|
||||||
<IconLucideChevronRight
|
|
||||||
class="h-4 w-4 transition-transform"
|
|
||||||
:class="openDropdown === 'products-mobile' ? 'rotate-90' : ''"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Transition name="nav-dropdown-mobile">
|
|
||||||
<ul
|
|
||||||
v-if="openDropdown === 'products-mobile'"
|
|
||||||
class="mt-2 space-y-1 rounded-md border border-base-200 bg-base-100 p-2 shadow-sm overflow-hidden"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/product-catalog"
|
|
||||||
class="rounded-md px-2 py-1 transition-colors block"
|
|
||||||
:class="
|
|
||||||
isActive('/product-catalog')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catalogue des produits
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/product-category"
|
|
||||||
class="rounded-md px-2 py-1 transition-colors block"
|
|
||||||
:class="
|
|
||||||
isActive('/product-category')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catégorie de produit
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Transition>
|
|
||||||
</li>
|
|
||||||
<li class="mt-1 border-t border-base-200 pt-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex w-full items-center justify-between rounded-md px-2 py-1 text-left transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/component-category') || isActive('/component-catalog')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
@click="toggleDropdown('component-mobile')"
|
|
||||||
@keydown.enter.prevent="toggleDropdown('component-mobile')"
|
|
||||||
@keydown.space.prevent="toggleDropdown('component-mobile')"
|
|
||||||
:aria-expanded="openDropdown === 'component-mobile'"
|
|
||||||
>
|
|
||||||
<span>Composant</span>
|
|
||||||
<IconLucideChevronRight
|
|
||||||
class="h-4 w-4 transition-transform"
|
|
||||||
:class="openDropdown === 'component-mobile' ? 'rotate-90' : ''"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Transition name="nav-dropdown-mobile">
|
|
||||||
<ul
|
|
||||||
v-if="openDropdown === 'component-mobile'"
|
|
||||||
class="mt-2 space-y-1 rounded-md border border-base-200 bg-base-100 p-2 shadow-sm overflow-hidden"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/component-catalog"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/component-catalog')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catalogue des composants
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/component-category"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/component-category')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catégorie de composant
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Transition>
|
|
||||||
</li>
|
|
||||||
<li class="mt-1 border-t border-base-200 pt-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex w-full items-center justify-between rounded-md px-2 py-1 text-left transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/sites') ||
|
|
||||||
isActive('/documents') ||
|
|
||||||
isActive('/constructeurs')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
@click="toggleDropdown('resources-mobile')"
|
|
||||||
@keydown.enter.prevent="toggleDropdown('resources-mobile')"
|
|
||||||
@keydown.space.prevent="toggleDropdown('resources-mobile')"
|
|
||||||
:aria-expanded="openDropdown === 'resources-mobile'"
|
|
||||||
>
|
|
||||||
<span>Ressources liées</span>
|
|
||||||
<IconLucideChevronRight
|
|
||||||
class="h-4 w-4 transition-transform"
|
|
||||||
:class="openDropdown === 'resources-mobile' ? 'rotate-90' : ''"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Transition name="nav-dropdown-mobile">
|
|
||||||
<ul
|
|
||||||
v-if="openDropdown === 'resources-mobile'"
|
|
||||||
class="mt-2 space-y-1 rounded-md border border-base-200 bg-base-100 p-2 shadow-sm overflow-hidden"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/sites"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/sites')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Sites
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/documents"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/documents')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Documents
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/constructeurs"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/constructeurs')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Fournisseurs
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Transition>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<div class="avatar">
|
|
||||||
<div class="w-14">
|
|
||||||
<img
|
|
||||||
:src="logoSrc"
|
|
||||||
alt="Logo Malio"
|
|
||||||
class="h-full w-full object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<NuxtLink to="/" class="btn btn-ghost text-xl">
|
|
||||||
Inventory
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="navbar-center hidden lg:flex">
|
|
||||||
<ul class="menu menu-horizontal px-1">
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="transition-colors px-3 py-2 rounded-md"
|
|
||||||
:class="
|
|
||||||
isActive('/')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Vue d'ensemble
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/machines"
|
|
||||||
class="transition-colors px-3 py-2 rounded-md"
|
|
||||||
:class="
|
|
||||||
isActive('/machines')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Parc Machines
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/machine-skeleton"
|
|
||||||
class="transition-colors px-3 py-2 rounded-md"
|
|
||||||
:class="
|
|
||||||
isActive('/machine-skeleton')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Squelettes de machine
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="relative"
|
|
||||||
@mouseenter="setDropdown('pieces-desktop')"
|
|
||||||
@mouseleave="scheduleDropdownClose('pieces-desktop')"
|
|
||||||
@focusin="setDropdown('pieces-desktop')"
|
|
||||||
@focusout="scheduleDropdownClose('pieces-desktop')"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center gap-1 rounded-md px-3 py-2 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/piece-category') || isActive('/pieces-catalog')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
@click="toggleDropdown('pieces-desktop')"
|
|
||||||
@keydown.enter.prevent="toggleDropdown('pieces-desktop')"
|
|
||||||
@keydown.space.prevent="toggleDropdown('pieces-desktop')"
|
|
||||||
:aria-expanded="openDropdown === 'pieces-desktop'"
|
|
||||||
>
|
|
||||||
Pièces
|
|
||||||
<IconLucideChevronRight
|
|
||||||
class="h-4 w-4 transition-transform"
|
|
||||||
:class="openDropdown === 'pieces-desktop' ? 'rotate-90' : ''"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Transition name="nav-dropdown-desktop">
|
|
||||||
<ul
|
|
||||||
v-if="openDropdown === 'pieces-desktop'"
|
|
||||||
class="absolute left-0 top-full mt-2 w-60 rounded-lg border border-base-200 bg-base-100 p-2 shadow-lg z-50"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/piece-category"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/piece-category')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catégorie de pièce
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/pieces-catalog"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/pieces-catalog')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catalogue des pièces
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Transition>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="relative"
|
|
||||||
@mouseenter="setDropdown('products-desktop')"
|
|
||||||
@mouseleave="scheduleDropdownClose('products-desktop')"
|
|
||||||
@focusin="setDropdown('products-desktop')"
|
|
||||||
@focusout="scheduleDropdownClose('products-desktop')"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center gap-1 rounded-md px-3 py-2 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/product-category') || isActive('/product-catalog')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
@click="toggleDropdown('products-desktop')"
|
|
||||||
@keydown.enter.prevent="toggleDropdown('products-desktop')"
|
|
||||||
@keydown.space.prevent="toggleDropdown('products-desktop')"
|
|
||||||
:aria-expanded="openDropdown === 'products-desktop'"
|
|
||||||
>
|
|
||||||
Produits
|
|
||||||
<IconLucideChevronRight
|
|
||||||
class="h-4 w-4 transition-transform"
|
|
||||||
:class="openDropdown === 'products-desktop' ? 'rotate-90' : ''"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Transition name="nav-dropdown-desktop">
|
|
||||||
<ul
|
|
||||||
v-if="openDropdown === 'products-desktop'"
|
|
||||||
class="absolute left-0 top-full mt-2 w-64 rounded-lg border border-base-200 bg-base-100 p-2 shadow-lg z-50"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/product-category"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/product-category')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catégorie de produit
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/product-catalog"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/product-catalog')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catalogue des produits
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Transition>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="relative"
|
|
||||||
@mouseenter="setDropdown('component-desktop')"
|
|
||||||
@mouseleave="scheduleDropdownClose('component-desktop')"
|
|
||||||
@focusin="setDropdown('component-desktop')"
|
|
||||||
@focusout="scheduleDropdownClose('component-desktop')"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center gap-1 rounded-md px-3 py-2 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/component-category') ||
|
|
||||||
isActive('/component-catalog')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
@click="toggleDropdown('component-desktop')"
|
|
||||||
@keydown.enter.prevent="toggleDropdown('component-desktop')"
|
|
||||||
@keydown.space.prevent="toggleDropdown('component-desktop')"
|
|
||||||
:aria-expanded="openDropdown === 'component-desktop'"
|
|
||||||
>
|
|
||||||
Composant
|
|
||||||
<IconLucideChevronRight
|
|
||||||
class="h-4 w-4 transition-transform"
|
|
||||||
:class="openDropdown === 'component-desktop' ? 'rotate-90' : ''"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Transition name="nav-dropdown-desktop">
|
|
||||||
<ul
|
|
||||||
v-if="openDropdown === 'component-desktop'"
|
|
||||||
class="absolute left-0 top-full mt-2 w-64 rounded-lg border border-base-200 bg-base-100 p-2 shadow-lg z-50"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/component-category"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/component-category')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catégorie de composant
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/component-catalog"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/component-catalog')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Catalogue des composants
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Transition>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="relative"
|
|
||||||
@mouseenter="setDropdown('resources-desktop')"
|
|
||||||
@mouseleave="scheduleDropdownClose('resources-desktop')"
|
|
||||||
@focusin="setDropdown('resources-desktop')"
|
|
||||||
@focusout="scheduleDropdownClose('resources-desktop')"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center gap-1 rounded-md px-3 py-2 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/sites') ||
|
|
||||||
isActive('/documents') ||
|
|
||||||
isActive('/constructeurs')
|
|
||||||
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
@click="toggleDropdown('resources-desktop')"
|
|
||||||
@keydown.enter.prevent="toggleDropdown('resources-desktop')"
|
|
||||||
@keydown.space.prevent="toggleDropdown('resources-desktop')"
|
|
||||||
:aria-expanded="openDropdown === 'resources-desktop'"
|
|
||||||
>
|
|
||||||
Ressources liées
|
|
||||||
<IconLucideChevronRight
|
|
||||||
class="h-4 w-4 transition-transform"
|
|
||||||
:class="openDropdown === 'resources-desktop' ? 'rotate-90' : ''"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Transition name="nav-dropdown-desktop">
|
|
||||||
<ul
|
|
||||||
v-if="openDropdown === 'resources-desktop'"
|
|
||||||
class="absolute left-0 top-full mt-2 w-60 rounded-lg border border-base-200 bg-base-100 p-2 shadow-lg z-50"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/sites"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/sites')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Sites
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/documents"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/documents')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Documents
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NuxtLink
|
|
||||||
to="/constructeurs"
|
|
||||||
class="block rounded-md px-2 py-1 transition-colors"
|
|
||||||
:class="
|
|
||||||
isActive('/constructeurs')
|
|
||||||
? 'bg-primary/10 text-primary font-semibold'
|
|
||||||
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Fournisseurs
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Transition>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="navbar-end">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<!-- Bouton paramètres d'affichage -->
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-circle hidden lg:inline-flex"
|
|
||||||
title="Paramètres d'affichage"
|
|
||||||
@click="openDisplaySettings"
|
|
||||||
>
|
|
||||||
<IconLucideSettings class="w-5 h-5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<div v-if="activeProfile" class="dropdown dropdown-end">
|
|
||||||
<div
|
|
||||||
tabindex="0"
|
|
||||||
role="button"
|
|
||||||
class="btn btn-ghost btn-circle avatar placeholder"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-secondary text-secondary-content rounded-full w-10 h-10 grid place-items-center"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="flex h-full w-full items-center justify-center text-sm font-semibold leading-none tracking-tight"
|
|
||||||
>
|
|
||||||
{{ activeProfileInitials }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
tabindex="0"
|
|
||||||
class="menu dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-64"
|
|
||||||
>
|
|
||||||
<li class="px-2 py-1 text-sm text-base-content/70">
|
|
||||||
Connecté en tant que<br />
|
|
||||||
<span class="font-semibold text-base-content">{{
|
|
||||||
activeProfileLabel
|
|
||||||
}}</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<NuxtLink to="/profiles/manage" class="justify-between">
|
|
||||||
Gestion des profils
|
|
||||||
<IconLucideChevronRight
|
|
||||||
class="w-4 h-4"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="text-error justify-between"
|
|
||||||
@click="handleLogout"
|
|
||||||
>
|
|
||||||
Déconnexion
|
|
||||||
<IconLucideLogOut class="w-4 h-4" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Page Content -->
|
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
|
|
||||||
<!-- Toast Notifications -->
|
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
<!-- Paramètres d'affichage -->
|
<CommonConfirmModal />
|
||||||
|
|
||||||
<DisplaySettings
|
<DisplaySettings
|
||||||
:is-open="displaySettingsOpen"
|
:is-open="displaySettingsOpen"
|
||||||
@close="closeDisplaySettings"
|
@close="displaySettingsOpen = false"
|
||||||
@update-settings="handleSettingsUpdate"
|
@update-settings="handleSettingsUpdate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="footer p-4 bg-neutral text-neutral-content">
|
<footer class="footer p-4 bg-neutral text-neutral-content">
|
||||||
<div class="items-center grid-flow-col">
|
<div class="items-center grid-flow-col">
|
||||||
<p>@Malio 2025 · v{{ appVersion }}</p>
|
<p>@Malio 2025 · v{{ appVersion }}</p>
|
||||||
@@ -701,161 +25,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute, navigateTo, useRuntimeConfig } from "#imports";
|
import { navigateTo, useRuntimeConfig } from '#imports'
|
||||||
import { useProfileSession } from "~/composables/useProfileSession";
|
import { useProfileSession } from '~/composables/useProfileSession'
|
||||||
import IconLucideMenu from "~icons/lucide/menu";
|
|
||||||
import IconLucideSettings from "~icons/lucide/settings";
|
|
||||||
import IconLucidePlus from "~icons/lucide/plus";
|
|
||||||
import IconLucideCpu from "~icons/lucide/cpu";
|
|
||||||
import IconLucideFilePlus from "~icons/lucide/file-plus";
|
|
||||||
import IconLucideMapPin from "~icons/lucide/map-pin";
|
|
||||||
import IconLucideChevronRight from "~icons/lucide/chevron-right";
|
|
||||||
import IconLucideLogOut from "~icons/lucide/log-out";
|
|
||||||
import logoSrc from "~/assets/LOGO_CARRE_BLANC.png";
|
|
||||||
|
|
||||||
// État du modal des paramètres d'affichage
|
const displaySettingsOpen = ref(false)
|
||||||
const displaySettingsOpen = ref(false);
|
const { ensureSession, logout } = useProfileSession()
|
||||||
const { activeProfile, ensureSession, logout } = useProfileSession();
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const runtimeConfig = useRuntimeConfig();
|
const appVersion = computed(() => (runtimeConfig.public?.appVersion as string) ?? '0.1.0')
|
||||||
const appVersion = computed(() => runtimeConfig.public?.appVersion ?? "0.1.0");
|
|
||||||
|
|
||||||
// Route active pour souligner l'onglet sélectionné dans la navbar
|
const handleSettingsUpdate = (_settings: unknown) => {
|
||||||
const route = useRoute();
|
// Placeholder for future persistence
|
||||||
const isActive = (path) => {
|
}
|
||||||
if (path === "/") {
|
|
||||||
return route.path === "/";
|
|
||||||
}
|
|
||||||
return route.path.startsWith(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ouvrir les paramètres d'affichage
|
|
||||||
const openDisplaySettings = () => {
|
|
||||||
displaySettingsOpen.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fermer les paramètres d'affichage
|
|
||||||
const closeDisplaySettings = () => {
|
|
||||||
displaySettingsOpen.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gérer les mises à jour des paramètres
|
|
||||||
const handleSettingsUpdate = (settings) => {
|
|
||||||
console.log("Paramètres d'affichage mis à jour:", settings);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout();
|
await logout()
|
||||||
await navigateTo("/profiles");
|
await navigateTo('/profiles')
|
||||||
};
|
}
|
||||||
|
|
||||||
const openDropdown = ref(null);
|
|
||||||
let dropdownCloseTimer = null;
|
|
||||||
|
|
||||||
const setDropdown = (name) => {
|
|
||||||
if (dropdownCloseTimer) {
|
|
||||||
clearTimeout(dropdownCloseTimer);
|
|
||||||
dropdownCloseTimer = null;
|
|
||||||
}
|
|
||||||
if (openDropdown.value !== name) {
|
|
||||||
openDropdown.value = name;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleDropdownClose = (name) => {
|
|
||||||
if (dropdownCloseTimer) {
|
|
||||||
clearTimeout(dropdownCloseTimer);
|
|
||||||
}
|
|
||||||
dropdownCloseTimer = setTimeout(() => {
|
|
||||||
if (openDropdown.value === name) {
|
|
||||||
openDropdown.value = null;
|
|
||||||
}
|
|
||||||
dropdownCloseTimer = null;
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeDropdownNow = () => {
|
|
||||||
if (dropdownCloseTimer) {
|
|
||||||
clearTimeout(dropdownCloseTimer);
|
|
||||||
dropdownCloseTimer = null;
|
|
||||||
}
|
|
||||||
openDropdown.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDropdown = (name) => {
|
|
||||||
if (openDropdown.value === name) {
|
|
||||||
closeDropdownNow();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDropdown(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.fullPath,
|
|
||||||
() => {
|
|
||||||
closeDropdownNow();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeProfileLabel = computed(() => {
|
|
||||||
if (!activeProfile.value) {
|
|
||||||
return "Profil inconnu";
|
|
||||||
}
|
|
||||||
return `${activeProfile.value.firstName} ${activeProfile.value.lastName}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeProfileInitials = computed(() => {
|
|
||||||
if (!activeProfile.value) {
|
|
||||||
return "??";
|
|
||||||
}
|
|
||||||
const { firstName = "", lastName = "" } = activeProfile.value;
|
|
||||||
return (
|
|
||||||
`${firstName.charAt(0) || ""}${lastName.charAt(0) || ""}`.toUpperCase() ||
|
|
||||||
"??"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await ensureSession();
|
await ensureSession()
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (dropdownCloseTimer) {
|
|
||||||
clearTimeout(dropdownCloseTimer);
|
|
||||||
dropdownCloseTimer = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.nav-dropdown-desktop-enter-active,
|
|
||||||
.nav-dropdown-desktop-leave-active {
|
|
||||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
||||||
}
|
|
||||||
.nav-dropdown-desktop-enter-from,
|
|
||||||
.nav-dropdown-desktop-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(0.25rem);
|
|
||||||
}
|
|
||||||
.nav-dropdown-desktop-enter-to,
|
|
||||||
.nav-dropdown-desktop-leave-from {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-dropdown-mobile-enter-active,
|
|
||||||
.nav-dropdown-mobile-leave-active {
|
|
||||||
transition: max-height 0.2s ease, opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
.nav-dropdown-mobile-enter-from,
|
|
||||||
.nav-dropdown-mobile-leave-to {
|
|
||||||
max-height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.nav-dropdown-mobile-enter-to,
|
|
||||||
.nav-dropdown-mobile-leave-from {
|
|
||||||
max-height: 12rem;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
368
app/components/layout/AppNavbar.vue
Normal file
368
app/components/layout/AppNavbar.vue
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
<template>
|
||||||
|
<div class="navbar bg-base-100 shadow-lg">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<!-- Mobile hamburger menu -->
|
||||||
|
<div class="dropdown">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||||
|
<IconLucideMenu class="w-5 h-5" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
|
||||||
|
>
|
||||||
|
<li class="pt-1 pb-2 lg:hidden">
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center gap-2 rounded-md px-2 py-1 transition-colors text-base-content hover:bg-primary/10 hover:text-primary"
|
||||||
|
@click="$emit('open-settings')"
|
||||||
|
>
|
||||||
|
<IconLucideSettings class="w-4 h-4" aria-hidden="true" />
|
||||||
|
Paramètres d'affichage
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Mobile: simple links -->
|
||||||
|
<li v-for="link in simpleLinks" :key="link.to">
|
||||||
|
<NuxtLink
|
||||||
|
:to="link.to"
|
||||||
|
class="rounded-md px-2 py-1 transition-colors"
|
||||||
|
:class="linkClass(link)"
|
||||||
|
>
|
||||||
|
{{ link.label }}
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Mobile: dropdown groups -->
|
||||||
|
<li
|
||||||
|
v-for="group in navGroups"
|
||||||
|
:key="group.id + '-mobile'"
|
||||||
|
class="mt-1 border-t border-base-200 pt-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-center justify-between rounded-md px-2 py-1 text-left transition-colors"
|
||||||
|
:class="groupClass(group)"
|
||||||
|
:aria-expanded="openDropdown === group.id + '-mobile'"
|
||||||
|
@click="toggleDropdown(group.id + '-mobile')"
|
||||||
|
@keydown.enter.prevent="toggleDropdown(group.id + '-mobile')"
|
||||||
|
@keydown.space.prevent="toggleDropdown(group.id + '-mobile')"
|
||||||
|
>
|
||||||
|
<span>{{ group.label }}</span>
|
||||||
|
<IconLucideChevronRight
|
||||||
|
class="h-4 w-4 transition-transform"
|
||||||
|
:class="openDropdown === group.id + '-mobile' ? 'rotate-90' : ''"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<Transition name="nav-dropdown-mobile">
|
||||||
|
<ul
|
||||||
|
v-if="openDropdown === group.id + '-mobile'"
|
||||||
|
class="mt-2 space-y-1 rounded-md border border-base-200 bg-base-100 p-2 shadow-sm overflow-hidden"
|
||||||
|
>
|
||||||
|
<li v-for="child in group.children" :key="child.to">
|
||||||
|
<NuxtLink
|
||||||
|
:to="child.to"
|
||||||
|
class="rounded-md px-2 py-1 transition-colors block"
|
||||||
|
:class="childLinkClass(child)"
|
||||||
|
>
|
||||||
|
{{ child.label }}
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Transition>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="avatar">
|
||||||
|
<div class="w-14">
|
||||||
|
<img
|
||||||
|
:src="logoSrc"
|
||||||
|
alt="Logo Malio"
|
||||||
|
class="h-full w-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NuxtLink to="/" class="btn btn-ghost text-xl">
|
||||||
|
Inventory
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop navbar -->
|
||||||
|
<div class="navbar-center hidden lg:flex">
|
||||||
|
<ul class="menu menu-horizontal px-1">
|
||||||
|
<!-- Desktop: simple links -->
|
||||||
|
<li v-for="link in simpleLinks" :key="link.to">
|
||||||
|
<NuxtLink
|
||||||
|
:to="link.to"
|
||||||
|
class="transition-colors px-3 py-2 rounded-md"
|
||||||
|
:class="linkClass(link)"
|
||||||
|
>
|
||||||
|
{{ link.label }}
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Desktop: dropdown groups -->
|
||||||
|
<li
|
||||||
|
v-for="group in navGroups"
|
||||||
|
:key="group.id + '-desktop'"
|
||||||
|
class="relative"
|
||||||
|
@mouseenter="setDropdown(group.id + '-desktop')"
|
||||||
|
@mouseleave="scheduleDropdownClose(group.id + '-desktop')"
|
||||||
|
@focusin="setDropdown(group.id + '-desktop')"
|
||||||
|
@focusout="scheduleDropdownClose(group.id + '-desktop')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-1 rounded-md px-3 py-2 transition-colors"
|
||||||
|
:class="groupClass(group)"
|
||||||
|
:aria-expanded="openDropdown === group.id + '-desktop'"
|
||||||
|
@click="toggleDropdown(group.id + '-desktop')"
|
||||||
|
@keydown.enter.prevent="toggleDropdown(group.id + '-desktop')"
|
||||||
|
@keydown.space.prevent="toggleDropdown(group.id + '-desktop')"
|
||||||
|
>
|
||||||
|
{{ group.label }}
|
||||||
|
<IconLucideChevronRight
|
||||||
|
class="h-4 w-4 transition-transform"
|
||||||
|
:class="openDropdown === group.id + '-desktop' ? 'rotate-90' : ''"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<Transition name="nav-dropdown-desktop">
|
||||||
|
<ul
|
||||||
|
v-if="openDropdown === group.id + '-desktop'"
|
||||||
|
class="absolute left-0 top-full mt-2 w-64 rounded-lg border border-base-200 bg-base-100 p-2 shadow-lg z-50"
|
||||||
|
>
|
||||||
|
<li v-for="child in group.children" :key="child.to">
|
||||||
|
<NuxtLink
|
||||||
|
:to="child.to"
|
||||||
|
class="block rounded-md px-2 py-1 transition-colors"
|
||||||
|
:class="childLinkClass(child)"
|
||||||
|
>
|
||||||
|
{{ child.label }}
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Transition>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navbar end -->
|
||||||
|
<div class="navbar-end">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-circle hidden lg:inline-flex"
|
||||||
|
title="Paramètres d'affichage"
|
||||||
|
@click="$emit('open-settings')"
|
||||||
|
>
|
||||||
|
<IconLucideSettings class="w-5 h-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<div v-if="activeProfile" class="dropdown dropdown-end">
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
class="btn btn-ghost btn-circle avatar placeholder"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-secondary text-secondary-content rounded-full w-10 h-10 grid place-items-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex h-full w-full items-center justify-center text-sm font-semibold leading-none tracking-tight"
|
||||||
|
>
|
||||||
|
{{ activeProfileInitials }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="menu dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-64"
|
||||||
|
>
|
||||||
|
<li class="px-2 py-1 text-sm text-base-content/70">
|
||||||
|
Connecté en tant que<br />
|
||||||
|
<span class="font-semibold text-base-content">{{ activeProfileLabel }}</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/profiles/manage" class="justify-between">
|
||||||
|
Gestion des profils
|
||||||
|
<IconLucideChevronRight class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-error justify-between"
|
||||||
|
@click="$emit('logout')"
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
<IconLucideLogOut class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from '#imports'
|
||||||
|
import { useNavDropdown } from '~/composables/useNavDropdown'
|
||||||
|
import { useProfileSession } from '~/composables/useProfileSession'
|
||||||
|
import IconLucideMenu from '~icons/lucide/menu'
|
||||||
|
import IconLucideSettings from '~icons/lucide/settings'
|
||||||
|
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
||||||
|
import IconLucideLogOut from '~icons/lucide/log-out'
|
||||||
|
import logoSrc from '~/assets/LOGO_CARRE_BLANC.png'
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'open-settings'): void
|
||||||
|
(e: 'logout'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
interface NavLink {
|
||||||
|
to: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavGroup {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
activePaths: string[]
|
||||||
|
children: NavLink[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const simpleLinks: NavLink[] = [
|
||||||
|
{ to: '/', label: 'Vue d\'ensemble' },
|
||||||
|
{ to: '/machines', label: 'Parc Machines' },
|
||||||
|
{ to: '/machine-skeleton', label: 'Squelettes de machine' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const navGroups: NavGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'pieces',
|
||||||
|
label: 'Pièces',
|
||||||
|
activePaths: ['/piece-category', '/pieces-catalog'],
|
||||||
|
children: [
|
||||||
|
{ to: '/pieces-catalog', label: 'Catalogue des pièces' },
|
||||||
|
{ to: '/piece-category', label: 'Catégorie de pièce' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'products',
|
||||||
|
label: 'Produits',
|
||||||
|
activePaths: ['/product-category', '/product-catalog'],
|
||||||
|
children: [
|
||||||
|
{ to: '/product-catalog', label: 'Catalogue des produits' },
|
||||||
|
{ to: '/product-category', label: 'Catégorie de produit' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'component',
|
||||||
|
label: 'Composant',
|
||||||
|
activePaths: ['/component-category', '/component-catalog'],
|
||||||
|
children: [
|
||||||
|
{ to: '/component-catalog', label: 'Catalogue des composants' },
|
||||||
|
{ to: '/component-category', label: 'Catégorie de composant' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'resources',
|
||||||
|
label: 'Ressources liées',
|
||||||
|
activePaths: ['/sites', '/documents', '/constructeurs'],
|
||||||
|
children: [
|
||||||
|
{ to: '/sites', label: 'Sites' },
|
||||||
|
{ to: '/documents', label: 'Documents' },
|
||||||
|
{ to: '/constructeurs', label: 'Fournisseurs' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const { openDropdown, setDropdown, scheduleDropdownClose, toggleDropdown } = useNavDropdown()
|
||||||
|
const { activeProfile } = useProfileSession()
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
if (path === '/') {
|
||||||
|
return route.path === '/'
|
||||||
|
}
|
||||||
|
return route.path.startsWith(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGroupActive = (group: NavGroup) => {
|
||||||
|
return group.activePaths.some((path) => isActive(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkClass = (link: NavLink) => {
|
||||||
|
return isActive(link.to)
|
||||||
|
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
||||||
|
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupClass = (group: NavGroup) => {
|
||||||
|
return isGroupActive(group)
|
||||||
|
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
||||||
|
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
const childLinkClass = (child: NavLink) => {
|
||||||
|
return isActive(child.to)
|
||||||
|
? 'bg-primary/10 text-primary font-semibold'
|
||||||
|
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeProfileLabel = computed(() => {
|
||||||
|
if (!activeProfile.value) {
|
||||||
|
return 'Profil inconnu'
|
||||||
|
}
|
||||||
|
return `${activeProfile.value.firstName} ${activeProfile.value.lastName}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeProfileInitials = computed(() => {
|
||||||
|
if (!activeProfile.value) {
|
||||||
|
return '??'
|
||||||
|
}
|
||||||
|
const { firstName = '', lastName = '' } = activeProfile.value
|
||||||
|
return (
|
||||||
|
`${firstName.charAt(0) || ''}${lastName.charAt(0) || ''}`.toUpperCase() || '??'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-dropdown-desktop-enter-active,
|
||||||
|
.nav-dropdown-desktop-leave-active {
|
||||||
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.nav-dropdown-desktop-enter-from,
|
||||||
|
.nav-dropdown-desktop-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(0.25rem);
|
||||||
|
}
|
||||||
|
.nav-dropdown-desktop-enter-to,
|
||||||
|
.nav-dropdown-desktop-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-mobile-enter-active,
|
||||||
|
.nav-dropdown-mobile-leave-active {
|
||||||
|
transition: max-height 0.2s ease, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.nav-dropdown-mobile-enter-from,
|
||||||
|
.nav-dropdown-mobile-leave-to {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.nav-dropdown-mobile-enter-to,
|
||||||
|
.nav-dropdown-mobile-leave-from {
|
||||||
|
max-height: 12rem;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
app/composables/useNavDropdown.ts
Normal file
65
app/composables/useNavDropdown.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { ref, watch, onUnmounted } from 'vue'
|
||||||
|
import { useRoute } from '#imports'
|
||||||
|
|
||||||
|
export function useNavDropdown() {
|
||||||
|
const openDropdown = ref<string | null>(null)
|
||||||
|
let dropdownCloseTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const setDropdown = (name: string) => {
|
||||||
|
if (dropdownCloseTimer) {
|
||||||
|
clearTimeout(dropdownCloseTimer)
|
||||||
|
dropdownCloseTimer = null
|
||||||
|
}
|
||||||
|
if (openDropdown.value !== name) {
|
||||||
|
openDropdown.value = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleDropdownClose = (name: string) => {
|
||||||
|
if (dropdownCloseTimer) {
|
||||||
|
clearTimeout(dropdownCloseTimer)
|
||||||
|
}
|
||||||
|
dropdownCloseTimer = setTimeout(() => {
|
||||||
|
if (openDropdown.value === name) {
|
||||||
|
openDropdown.value = null
|
||||||
|
}
|
||||||
|
dropdownCloseTimer = null
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDropdownNow = () => {
|
||||||
|
if (dropdownCloseTimer) {
|
||||||
|
clearTimeout(dropdownCloseTimer)
|
||||||
|
dropdownCloseTimer = null
|
||||||
|
}
|
||||||
|
openDropdown.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDropdown = (name: string) => {
|
||||||
|
if (openDropdown.value === name) {
|
||||||
|
closeDropdownNow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDropdown(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => route.fullPath, () => {
|
||||||
|
closeDropdownNow()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (dropdownCloseTimer) {
|
||||||
|
clearTimeout(dropdownCloseTimer)
|
||||||
|
dropdownCloseTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
openDropdown,
|
||||||
|
setDropdown,
|
||||||
|
scheduleDropdownClose,
|
||||||
|
closeDropdownNow,
|
||||||
|
toggleDropdown,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user