feat: Réorganiser la navigation admin en catégories pour améliorer l'UX mobile-first
Le menu d'administration contenait 13 liens à plat dans le header, ce qui débordait sur desktop et rendait le drawer mobile trop long à scanner. Les liens sont maintenant regroupés en 4 catégories (Personnes, Organisation, Année scolaire, Paramètres) avec des dropdowns au survol sur desktop et des accordéons repliables dans le drawer mobile. Le nombre d'éléments visibles passe de 13 à 5 (1 lien direct + 4 catégories), la catégorie active s'auto-déplie dans le menu mobile.
This commit is contained in:
@@ -119,6 +119,7 @@ test.describe('Admin Responsive Navigation', () => {
|
||||
const drawer = page.locator('[role="dialog"][aria-modal="true"]');
|
||||
await expect(drawer).toBeVisible();
|
||||
|
||||
// Active category "Personnes" should be auto-expanded
|
||||
const activeLink = drawer.locator('.mobile-nav-link.active');
|
||||
await expect(activeLink).toHaveText('Utilisateurs');
|
||||
});
|
||||
@@ -128,11 +129,13 @@ test.describe('Admin Responsive Navigation', () => {
|
||||
await page.goto(`${ALPHA_URL}/admin/users`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Open menu and click Classes
|
||||
// Open menu and expand "Organisation" section to find "Classes"
|
||||
await page.getByRole('button', { name: /ouvrir le menu/i }).click();
|
||||
const drawer = page.locator('[role="dialog"][aria-modal="true"]');
|
||||
await expect(drawer).toBeVisible();
|
||||
|
||||
// Expand "Organisation" accordion
|
||||
await drawer.getByRole('button', { name: 'Organisation' }).click();
|
||||
await drawer.getByRole('link', { name: 'Classes' }).click();
|
||||
|
||||
// Menu should close and page should navigate
|
||||
@@ -143,6 +146,30 @@ test.describe('Admin Responsive Navigation', () => {
|
||||
const label = page.locator('.mobile-section-label');
|
||||
await expect(label).toHaveText('Classes');
|
||||
});
|
||||
|
||||
test('accordion sections expand and collapse', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/users`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.getByRole('button', { name: /ouvrir le menu/i }).click();
|
||||
const drawer = page.locator('[role="dialog"][aria-modal="true"]');
|
||||
await expect(drawer).toBeVisible();
|
||||
|
||||
// "Personnes" should be auto-expanded (active category)
|
||||
await expect(drawer.getByRole('link', { name: 'Utilisateurs' })).toBeVisible();
|
||||
|
||||
// "Organisation" should be collapsed initially
|
||||
await expect(drawer.getByRole('link', { name: 'Classes' })).not.toBeVisible();
|
||||
|
||||
// Expand "Organisation"
|
||||
await drawer.getByRole('button', { name: 'Organisation' }).click();
|
||||
await expect(drawer.getByRole('link', { name: 'Classes' })).toBeVisible();
|
||||
|
||||
// Collapse "Organisation"
|
||||
await drawer.getByRole('button', { name: 'Organisation' }).click();
|
||||
await expect(drawer.getByRole('link', { name: 'Classes' })).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
@@ -163,7 +190,7 @@ test.describe('Admin Responsive Navigation', () => {
|
||||
await expect(desktopNav).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('drawer opens and works', async ({ page }) => {
|
||||
test('drawer opens and shows grouped nav', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/users`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
@@ -172,8 +199,11 @@ test.describe('Admin Responsive Navigation', () => {
|
||||
const drawer = page.locator('[role="dialog"][aria-modal="true"]');
|
||||
await expect(drawer).toBeVisible();
|
||||
|
||||
// All nav links should be visible in drawer
|
||||
// "Personnes" auto-expanded (contains active link Utilisateurs)
|
||||
await expect(drawer.getByRole('link', { name: 'Utilisateurs' })).toBeVisible();
|
||||
|
||||
// Expand "Organisation" to see its links
|
||||
await drawer.getByRole('button', { name: 'Organisation' }).click();
|
||||
await expect(drawer.getByRole('link', { name: 'Classes' })).toBeVisible();
|
||||
await expect(drawer.getByRole('link', { name: 'Matières' })).toBeVisible();
|
||||
});
|
||||
@@ -197,18 +227,42 @@ test.describe('Admin Responsive Navigation', () => {
|
||||
await expect(desktopNav).toBeVisible();
|
||||
});
|
||||
|
||||
test('desktop nav shows all navigation links', async ({ page }) => {
|
||||
test('desktop nav shows category dropdowns', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/users`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const nav = page.locator('.desktop-nav');
|
||||
await expect(nav.getByRole('link', { name: 'Utilisateurs' })).toBeVisible();
|
||||
await expect(nav.getByRole('link', { name: 'Classes' })).toBeVisible();
|
||||
await expect(nav.getByRole('link', { name: 'Matières' })).toBeVisible();
|
||||
await expect(nav.getByRole('link', { name: 'Affectations' })).toBeVisible();
|
||||
await expect(nav.getByRole('link', { name: 'Périodes' })).toBeVisible();
|
||||
await expect(nav.getByRole('link', { name: 'Pédagogie' })).toBeVisible();
|
||||
|
||||
// Category triggers should be visible
|
||||
await expect(nav.getByRole('button', { name: /personnes/i })).toBeVisible();
|
||||
await expect(nav.getByRole('button', { name: /organisation/i })).toBeVisible();
|
||||
await expect(nav.getByRole('button', { name: /année scolaire/i })).toBeVisible();
|
||||
await expect(nav.getByRole('button', { name: /paramètres/i })).toBeVisible();
|
||||
|
||||
// Hover "Personnes" to reveal dropdown
|
||||
await nav.getByRole('button', { name: /personnes/i }).hover();
|
||||
const dropdown = nav.locator('.dropdown-panel').first();
|
||||
await expect(dropdown).toBeVisible();
|
||||
await expect(dropdown.getByRole('menuitem', { name: 'Utilisateurs' })).toBeVisible();
|
||||
await expect(dropdown.getByRole('menuitem', { name: 'Élèves' })).toBeVisible();
|
||||
|
||||
// Hover "Organisation"
|
||||
await nav.getByRole('button', { name: /organisation/i }).hover();
|
||||
const orgDropdown = nav.locator('.dropdown-panel').first();
|
||||
await expect(orgDropdown.getByRole('menuitem', { name: 'Classes' })).toBeVisible();
|
||||
await expect(orgDropdown.getByRole('menuitem', { name: 'Matières' })).toBeVisible();
|
||||
await expect(orgDropdown.getByRole('menuitem', { name: 'Affectations' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('active category trigger is highlighted', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/users`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const nav = page.locator('.desktop-nav');
|
||||
const personnesTrigger = nav.getByRole('button', { name: /personnes/i });
|
||||
await expect(personnesTrigger).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
test('hides mobile section label', async ({ page }) => {
|
||||
|
||||
@@ -83,7 +83,10 @@ test.describe('Calendar Management (Story 2.11)', () => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/classes`);
|
||||
|
||||
await page.getByRole('link', { name: /calendrier/i }).click();
|
||||
// Hover "Année scolaire" category to reveal dropdown
|
||||
const nav = page.locator('.desktop-nav');
|
||||
await nav.getByRole('button', { name: /année scolaire/i }).hover();
|
||||
await nav.getByRole('menuitem', { name: /calendrier/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/admin\/calendar/);
|
||||
await expect(
|
||||
|
||||
@@ -59,7 +59,10 @@ test.describe('Pedagogy - Grading Mode Configuration (Story 2.4)', () => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/classes`);
|
||||
|
||||
const pedagogyLink = page.getByRole('link', { name: /pédagogie/i });
|
||||
// Hover "Paramètres" category to reveal dropdown
|
||||
const nav = page.locator('.desktop-nav');
|
||||
await nav.getByRole('button', { name: /paramètres/i }).hover();
|
||||
const pedagogyLink = nav.getByRole('menuitem', { name: /pédagogie/i });
|
||||
await expect(pedagogyLink).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -67,7 +70,10 @@ test.describe('Pedagogy - Grading Mode Configuration (Story 2.4)', () => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/classes`);
|
||||
|
||||
await page.getByRole('link', { name: /pédagogie/i }).click();
|
||||
// Hover "Paramètres" category to reveal dropdown
|
||||
const nav = page.locator('.desktop-nav');
|
||||
await nav.getByRole('button', { name: /paramètres/i }).hover();
|
||||
await nav.getByRole('menuitem', { name: /pédagogie/i }).click();
|
||||
await expect(page).toHaveURL(/\/admin\/pedagogy/, { timeout: 10000 });
|
||||
});
|
||||
|
||||
|
||||
@@ -288,8 +288,10 @@ test.describe('Periods Management (Story 2.3)', () => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin`);
|
||||
|
||||
// Click on periods link in the admin navigation
|
||||
await page.getByRole('link', { name: /périodes/i }).click();
|
||||
// Hover "Année scolaire" category to reveal dropdown
|
||||
const nav = page.locator('.desktop-nav');
|
||||
await nav.getByRole('button', { name: /année scolaire/i }).hover();
|
||||
await nav.getByRole('menuitem', { name: /périodes/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/admin\/academic-year\/periods/);
|
||||
await expect(page.getByRole('heading', { name: /périodes scolaires/i })).toBeVisible();
|
||||
|
||||
@@ -184,10 +184,14 @@ test.describe('Role-Based Access Control [P0]', () => {
|
||||
await loginAs(page, ADMIN_EMAIL, ADMIN_PASSWORD);
|
||||
await page.goto(`${ALPHA_URL}/admin`);
|
||||
|
||||
// Admin layout should show navigation links (scoped to desktop nav to avoid action cards)
|
||||
// Admin layout should show grouped navigation (category triggers in desktop nav)
|
||||
const nav = page.locator('.desktop-nav');
|
||||
await expect(nav.getByRole('link', { name: 'Utilisateurs' })).toBeVisible({ timeout: 15000 });
|
||||
await expect(nav.getByRole('link', { name: 'Classes' })).toBeVisible();
|
||||
await expect(nav.getByRole('button', { name: /personnes/i })).toBeVisible({ timeout: 15000 });
|
||||
await expect(nav.getByRole('button', { name: /organisation/i })).toBeVisible();
|
||||
|
||||
// Hover to reveal dropdown links
|
||||
await nav.getByRole('button', { name: /personnes/i }).hover();
|
||||
await expect(nav.getByRole('menuitem', { name: 'Utilisateurs' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[P0] teacher sees dashboard without admin navigation', async ({ page }) => {
|
||||
|
||||
@@ -145,8 +145,10 @@ test.describe('Student Creation & Management (Story 3.0)', () => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/students`);
|
||||
|
||||
// The nav should have an active "Élèves" link
|
||||
await expect(page.locator('nav a', { hasText: /élèves/i })).toBeVisible({
|
||||
// Hover "Personnes" category to reveal dropdown with "Élèves" link
|
||||
const nav = page.locator('.desktop-nav');
|
||||
await nav.getByRole('button', { name: /personnes/i }).hover();
|
||||
await expect(nav.getByRole('menuitem', { name: /élèves/i })).toBeVisible({
|
||||
timeout: 10000
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,7 +118,10 @@ test.describe('Teacher Assignments (Story 2.8)', () => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin`);
|
||||
|
||||
const navLink = page.getByRole('link', { name: /affectations/i });
|
||||
// Hover "Organisation" category to reveal dropdown
|
||||
const nav = page.locator('.desktop-nav');
|
||||
await nav.getByRole('button', { name: /organisation/i }).hover();
|
||||
const navLink = nav.getByRole('menuitem', { name: /affectations/i });
|
||||
await expect(navLink).toBeVisible({ timeout: 15000 });
|
||||
});
|
||||
|
||||
|
||||
@@ -121,7 +121,10 @@ test.describe('Teacher Replacements (Story 2.9)', () => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin`);
|
||||
|
||||
const navLink = page.getByRole('link', { name: /remplacements/i });
|
||||
// Hover "Organisation" category to reveal dropdown
|
||||
const nav = page.locator('.desktop-nav');
|
||||
await nav.getByRole('button', { name: /organisation/i }).hover();
|
||||
const navLink = nav.getByRole('menuitem', { name: /remplacements/i });
|
||||
await expect(navLink).toBeVisible({ timeout: 15000 });
|
||||
});
|
||||
|
||||
|
||||
@@ -22,20 +22,53 @@
|
||||
'ROLE_SECRETARIAT'
|
||||
];
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/dashboard', label: 'Tableau de bord', isActive: () => false },
|
||||
{ href: '/admin/users', label: 'Utilisateurs', isActive: () => isUsersActive },
|
||||
{ href: '/admin/parent-invitations', label: 'Invitations parents', isActive: () => isParentInvitationsActive },
|
||||
{ href: '/admin/students', label: 'Élèves', isActive: () => isStudentsActive },
|
||||
{ href: '/admin/classes', label: 'Classes', isActive: () => isClassesActive },
|
||||
{ href: '/admin/subjects', label: 'Matières', isActive: () => isSubjectsActive },
|
||||
{ href: '/admin/assignments', label: 'Affectations', isActive: () => isAssignmentsActive },
|
||||
{ href: '/admin/replacements', label: 'Remplacements', isActive: () => isReplacementsActive },
|
||||
{ href: '/admin/academic-year/periods', label: 'Périodes', isActive: () => isPeriodsActive },
|
||||
{ href: '/admin/calendar', label: 'Calendrier', isActive: () => isCalendarActive },
|
||||
{ href: '/admin/image-rights', label: 'Droit à l\'image', isActive: () => isImageRightsActive },
|
||||
{ href: '/admin/pedagogy', label: 'Pédagogie', isActive: () => isPedagogyActive },
|
||||
{ href: '/admin/branding', label: 'Identité visuelle', isActive: () => isBrandingActive }
|
||||
// Types
|
||||
type NavLink = { href: string; label: string };
|
||||
type NavCategory = { id: string; label: string; links: NavLink[] };
|
||||
type NavEntry = NavLink | NavCategory;
|
||||
|
||||
function isCategory(entry: NavEntry): entry is NavCategory {
|
||||
return 'links' in entry;
|
||||
}
|
||||
|
||||
const navEntries: NavEntry[] = [
|
||||
{ href: '/dashboard', label: 'Tableau de bord' },
|
||||
{
|
||||
id: 'personnes',
|
||||
label: 'Personnes',
|
||||
links: [
|
||||
{ href: '/admin/users', label: 'Utilisateurs' },
|
||||
{ href: '/admin/parent-invitations', label: 'Invitations parents' },
|
||||
{ href: '/admin/students', label: 'Élèves' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'organisation',
|
||||
label: 'Organisation',
|
||||
links: [
|
||||
{ href: '/admin/classes', label: 'Classes' },
|
||||
{ href: '/admin/subjects', label: 'Matières' },
|
||||
{ href: '/admin/assignments', label: 'Affectations' },
|
||||
{ href: '/admin/replacements', label: 'Remplacements' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'annee',
|
||||
label: 'Année scolaire',
|
||||
links: [
|
||||
{ href: '/admin/academic-year/periods', label: 'Périodes' },
|
||||
{ href: '/admin/calendar', label: 'Calendrier' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'parametres',
|
||||
label: 'Paramètres',
|
||||
links: [
|
||||
{ href: '/admin/image-rights', label: "Droit à l'image" },
|
||||
{ href: '/admin/pedagogy', label: 'Pédagogie' },
|
||||
{ href: '/admin/branding', label: 'Identité visuelle' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Load user roles and verify admin access
|
||||
@@ -81,43 +114,81 @@
|
||||
mobileMenuOpen = false;
|
||||
}
|
||||
|
||||
// Determine which admin section is active
|
||||
const isUsersActive = $derived(page.url.pathname.startsWith('/admin/users'));
|
||||
const isParentInvitationsActive = $derived(page.url.pathname.startsWith('/admin/parent-invitations'));
|
||||
const isStudentsActive = $derived(page.url.pathname.startsWith('/admin/students'));
|
||||
const isClassesActive = $derived(page.url.pathname.startsWith('/admin/classes'));
|
||||
const isSubjectsActive = $derived(page.url.pathname.startsWith('/admin/subjects'));
|
||||
const isPeriodsActive = $derived(page.url.pathname.startsWith('/admin/academic-year/periods'));
|
||||
const isAssignmentsActive = $derived(page.url.pathname.startsWith('/admin/assignments'));
|
||||
const isReplacementsActive = $derived(page.url.pathname.startsWith('/admin/replacements'));
|
||||
const isCalendarActive = $derived(page.url.pathname.startsWith('/admin/calendar'));
|
||||
const isImageRightsActive = $derived(page.url.pathname.startsWith('/admin/image-rights'));
|
||||
const isPedagogyActive = $derived(page.url.pathname.startsWith('/admin/pedagogy'));
|
||||
const isBrandingActive = $derived(page.url.pathname.startsWith('/admin/branding'));
|
||||
// Active link detection
|
||||
const activeLinkHref = $derived.by(() => {
|
||||
const path = page.url.pathname;
|
||||
for (const entry of navEntries) {
|
||||
if (isCategory(entry)) {
|
||||
for (const link of entry.links) {
|
||||
if (path.startsWith(link.href)) return link.href;
|
||||
}
|
||||
} else if (entry.href !== '/dashboard' && path.startsWith(entry.href)) {
|
||||
return entry.href;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const activeCategoryId = $derived.by(() => {
|
||||
if (!activeLinkHref) return null;
|
||||
for (const entry of navEntries) {
|
||||
if (isCategory(entry) && entry.links.some((l) => l.href === activeLinkHref)) {
|
||||
return entry.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const currentSectionLabel = $derived.by(() => {
|
||||
const path = page.url.pathname;
|
||||
for (const link of navLinks) {
|
||||
if (link.href !== '/dashboard' && path.startsWith(link.href)) {
|
||||
return link.label;
|
||||
for (const entry of navEntries) {
|
||||
if (isCategory(entry)) {
|
||||
for (const link of entry.links) {
|
||||
if (path.startsWith(link.href)) return link.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'Administration';
|
||||
});
|
||||
|
||||
// Close menu on route change
|
||||
// Desktop dropdown state
|
||||
let openDesktopCategory = $state<string | null>(null);
|
||||
|
||||
// Mobile accordion state
|
||||
let openMobileCategories = $state<Set<string>>(new Set());
|
||||
|
||||
function toggleMobileCategory(id: string) {
|
||||
const next = new Set(openMobileCategories);
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
openMobileCategories = next;
|
||||
}
|
||||
|
||||
// Auto-expand active category in mobile when drawer opens
|
||||
$effect(() => {
|
||||
if (mobileMenuOpen && activeCategoryId) {
|
||||
openMobileCategories = new Set([activeCategoryId]);
|
||||
}
|
||||
});
|
||||
|
||||
// Close menus on route change
|
||||
$effect(() => {
|
||||
void page.url.pathname;
|
||||
mobileMenuOpen = false;
|
||||
openDesktopCategory = null;
|
||||
});
|
||||
|
||||
// Close menu on Escape key
|
||||
// Close on Escape key
|
||||
$effect(() => {
|
||||
if (!mobileMenuOpen) return;
|
||||
if (!mobileMenuOpen && !openDesktopCategory) return;
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
mobileMenuOpen = false;
|
||||
if (openDesktopCategory) openDesktopCategory = null;
|
||||
if (mobileMenuOpen) mobileMenuOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +196,7 @@
|
||||
return () => document.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
// Lock body scroll when menu is open
|
||||
// Lock body scroll when mobile menu is open
|
||||
$effect(() => {
|
||||
if (mobileMenuOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
@@ -169,10 +240,52 @@
|
||||
|
||||
<nav class="desktop-nav">
|
||||
<RoleSwitcher />
|
||||
{#each navLinks as link}
|
||||
<a href={link.href} class="nav-link" class:active={link.isActive()}>{link.label}</a>
|
||||
{#each navEntries as entry}
|
||||
{#if isCategory(entry)}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="dropdown-wrapper"
|
||||
onmouseenter={() => (openDesktopCategory = entry.id)}
|
||||
onmouseleave={() => (openDesktopCategory = null)}
|
||||
>
|
||||
<button
|
||||
class="nav-link dropdown-trigger"
|
||||
class:active={activeCategoryId === entry.id}
|
||||
aria-expanded={openDesktopCategory === entry.id}
|
||||
aria-haspopup="true"
|
||||
onclick={() =>
|
||||
(openDesktopCategory =
|
||||
openDesktopCategory === entry.id ? null : entry.id)}
|
||||
>
|
||||
{entry.label}
|
||||
<span
|
||||
class="dropdown-chevron"
|
||||
class:open={openDesktopCategory === entry.id}
|
||||
>
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
{#if openDesktopCategory === entry.id}
|
||||
<div class="dropdown-panel" role="menu">
|
||||
{#each entry.links as link}
|
||||
<a
|
||||
href={link.href}
|
||||
class="dropdown-link"
|
||||
class:active={activeLinkHref === link.href}
|
||||
role="menuitem"
|
||||
onclick={() => (openDesktopCategory = null)}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
{/each}
|
||||
<button class="nav-button" onclick={goSettings}>Paramètres</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<a href={entry.href} class="nav-link">{entry.label}</a>
|
||||
{/if}
|
||||
{/each}
|
||||
<button class="nav-button" onclick={goSettings}>Réglages</button>
|
||||
<button class="logout-button" onclick={handleLogout} disabled={isLoggingOut}>
|
||||
{#if isLoggingOut}
|
||||
<span class="spinner"></span>
|
||||
@@ -215,18 +328,49 @@
|
||||
<div class="mobile-role-switcher">
|
||||
<RoleSwitcher />
|
||||
</div>
|
||||
{#each navLinks as link}
|
||||
{#each navEntries as entry}
|
||||
{#if isCategory(entry)}
|
||||
<div class="mobile-section">
|
||||
<button
|
||||
class="mobile-section-toggle"
|
||||
class:active={activeCategoryId === entry.id}
|
||||
aria-expanded={openMobileCategories.has(entry.id)}
|
||||
onclick={() => toggleMobileCategory(entry.id)}
|
||||
>
|
||||
{entry.label}
|
||||
<span
|
||||
class="accordion-chevron"
|
||||
class:open={openMobileCategories.has(entry.id)}
|
||||
>
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
{#if openMobileCategories.has(entry.id)}
|
||||
<div class="mobile-section-links">
|
||||
{#each entry.links as link}
|
||||
<a
|
||||
href={link.href}
|
||||
class="mobile-nav-link"
|
||||
class:active={link.isActive()}
|
||||
class:active={activeLinkHref === link.href}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<a
|
||||
href={entry.href}
|
||||
class="mobile-nav-link mobile-standalone-link"
|
||||
>
|
||||
{entry.label}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mobile-drawer-footer">
|
||||
<button class="mobile-nav-link" onclick={goSettings}>Paramètres</button>
|
||||
<button class="mobile-nav-link" onclick={goSettings}>Réglages</button>
|
||||
<button
|
||||
class="mobile-nav-link mobile-logout"
|
||||
onclick={handleLogout}
|
||||
@@ -378,6 +522,65 @@
|
||||
background: var(--accent-primary-light, #e0f2fe);
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.dropdown-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-chevron {
|
||||
font-size: 0.625rem;
|
||||
transition: transform 0.2s;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dropdown-chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown-panel {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 200px;
|
||||
background: var(--surface-elevated, #fff);
|
||||
border: 1px solid var(--border-subtle, #e2e8f0);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 0.375rem 0;
|
||||
z-index: 150;
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.dropdown-link {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #64748b);
|
||||
text-decoration: none;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-link:hover {
|
||||
color: var(--text-primary, #1f2937);
|
||||
background: var(--surface-primary, #f8fafc);
|
||||
}
|
||||
|
||||
.dropdown-link.active {
|
||||
color: var(--accent-primary, #0ea5e9);
|
||||
background: var(--accent-primary-light, #e0f2fe);
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
padding: 0.375rem 0.625rem;
|
||||
font-size: 0.8125rem;
|
||||
@@ -486,6 +689,63 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Mobile accordion sections */
|
||||
.mobile-section {
|
||||
border-bottom: 1px solid var(--border-subtle, #e2e8f0);
|
||||
}
|
||||
|
||||
.mobile-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mobile-section-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-secondary, #64748b);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.mobile-section-toggle:hover {
|
||||
background: var(--surface-primary, #f8fafc);
|
||||
color: var(--text-primary, #1f2937);
|
||||
}
|
||||
|
||||
.mobile-section-toggle.active {
|
||||
color: var(--accent-primary, #0ea5e9);
|
||||
}
|
||||
|
||||
.accordion-chevron {
|
||||
font-size: 0.625rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.accordion-chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.mobile-section-links {
|
||||
padding-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.mobile-section-links .mobile-nav-link {
|
||||
padding-left: 2rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.mobile-standalone-link {
|
||||
border-bottom: 1px solid var(--border-subtle, #e2e8f0);
|
||||
}
|
||||
|
||||
.mobile-nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -527,13 +787,21 @@
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from { transform: translateX(-100%); }
|
||||
to { transform: translateX(0); }
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
|
||||
Reference in New Issue
Block a user