7
0
forked from yvv/decision

Menu : Boîte à outils en premier + sidebar collapsible icônes

- Boîte à outils (/tools) en 1ère position dans la nav (desktop + mobile)
- Sidebar desktop repliable (toggle pictos-only, 14rem → 3.75rem)
  - labels masqués par transition CSS fluide (max-width + opacity)
  - état persisté en localStorage (libred-sidebar-collapsed)
- Page document : toggle Vue structurée / Aperçu document
  - sous-toggle En vigueur / Selon les votes
  - composant DocumentPreview (rendu PDF-like)
    - filigrane discret, items ordonnés par sort_order
    - mode projection : proposed_text substitu + encadrement orange
    - footer horodaté, print-friendly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-03-17 04:21:21 +01:00
parent 73c5bf148c
commit feaf4de7e0
4 changed files with 671 additions and 8 deletions

View File

@@ -4,6 +4,11 @@ const route = useRoute()
const { initMood } = useMood()
const navigationItems = [
{
label: 'Boîte à outils',
icon: 'i-lucide-wrench',
to: '/tools',
},
{
label: 'Documents',
icon: 'i-lucide-book-open',
@@ -34,6 +39,9 @@ const navigationItems = [
/** Mobile drawer state. */
const mobileMenuOpen = ref(false)
/** Sidebar collapse state (icons-only mode). */
const sidebarCollapsed = ref(false)
/** Close mobile menu on route change. */
watch(() => route.path, () => {
mobileMenuOpen.value = false
@@ -43,8 +51,14 @@ watch(() => route.path, () => {
const ws = useWebSocket()
const { setupWsNotifications } = useNotifications()
watch(sidebarCollapsed, (val) => {
localStorage.setItem('libred-sidebar-collapsed', String(val))
})
onMounted(async () => {
initMood()
const savedCollapsed = localStorage.getItem('libred-sidebar-collapsed')
if (savedCollapsed !== null) sidebarCollapsed.value = savedCollapsed === 'true'
auth.hydrateFromStorage()
if (auth.token) {
try {
@@ -177,7 +191,7 @@ function isActive(to: string) {
<!-- Main content with sidebar -->
<div class="app-body">
<!-- Desktop sidebar -->
<aside class="app-sidebar">
<aside class="app-sidebar" :class="{ 'app-sidebar--collapsed': sidebarCollapsed }">
<nav class="app-sidebar__nav">
<NuxtLink
v-for="item in navigationItems"
@@ -186,9 +200,21 @@ function isActive(to: string) {
class="app-sidebar__link"
:class="{ 'app-sidebar__link--active': isActive(item.to) }"
>
<UIcon :name="item.icon" class="text-lg" />
<span>{{ item.label }}</span>
<UIcon :name="item.icon" class="text-lg flex-shrink-0" />
<span class="app-sidebar__link-label">{{ item.label }}</span>
</NuxtLink>
<div class="app-sidebar__divider" />
<button
class="app-sidebar__toggle"
:title="sidebarCollapsed ? 'Déplier le menu' : 'Replier le menu'"
@click="sidebarCollapsed = !sidebarCollapsed"
>
<UIcon
:name="sidebarCollapsed ? 'i-lucide-panel-left-open' : 'i-lucide-panel-left-close'"
class="text-base flex-shrink-0"
/>
<span class="app-sidebar__link-label">Replier</span>
</button>
</nav>
</aside>
@@ -459,6 +485,12 @@ function isActive(to: string) {
flex-shrink: 0;
background: var(--mood-surface);
display: none;
transition: width 0.22s ease;
overflow: hidden;
}
.app-sidebar--collapsed {
width: 3.75rem;
}
@media (min-width: 768px) {
@@ -470,7 +502,7 @@ function isActive(to: string) {
.app-sidebar__nav {
position: sticky;
top: 3.5rem;
padding: 1rem 0.75rem;
padding: 1rem 0.5rem;
display: flex;
flex-direction: column;
gap: 4px;
@@ -480,13 +512,15 @@ function isActive(to: string) {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.625rem 0.875rem;
padding: 0.625rem 0.75rem;
font-size: 0.9375rem;
font-weight: 600;
color: var(--mood-text-muted);
text-decoration: none;
border-radius: 12px;
transition: all 0.12s ease;
white-space: nowrap;
overflow: hidden;
}
.app-sidebar__link:hover {
@@ -500,6 +534,56 @@ function isActive(to: string) {
font-weight: 700;
}
.app-sidebar__link-label {
overflow: hidden;
white-space: nowrap;
transition: opacity 0.18s ease, max-width 0.22s ease;
max-width: 10rem;
}
.app-sidebar--collapsed .app-sidebar__link-label {
opacity: 0;
max-width: 0;
}
.app-sidebar--collapsed .app-sidebar__link {
justify-content: center;
padding: 0.625rem;
}
.app-sidebar__divider {
height: 1px;
background: color-mix(in srgb, var(--mood-accent) 10%, transparent);
margin: 0.375rem 0.25rem;
}
.app-sidebar__toggle {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.5rem 0.75rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--mood-text-muted);
background: none;
cursor: pointer;
border-radius: 12px;
width: 100%;
transition: all 0.12s ease;
white-space: nowrap;
overflow: hidden;
}
.app-sidebar__toggle:hover {
color: var(--mood-text);
background: var(--mood-accent-soft);
}
.app-sidebar--collapsed .app-sidebar__toggle {
justify-content: center;
padding: 0.5rem;
}
/* === Mobile nav === */
.app-mobile-nav {
display: flex;