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:
2026-02-28 00:09:20 +01:00
parent be1b0b60a6
commit ce05207c64
9 changed files with 418 additions and 73 deletions

View File

@@ -119,6 +119,7 @@ test.describe('Admin Responsive Navigation', () => {
const drawer = page.locator('[role="dialog"][aria-modal="true"]'); const drawer = page.locator('[role="dialog"][aria-modal="true"]');
await expect(drawer).toBeVisible(); await expect(drawer).toBeVisible();
// Active category "Personnes" should be auto-expanded
const activeLink = drawer.locator('.mobile-nav-link.active'); const activeLink = drawer.locator('.mobile-nav-link.active');
await expect(activeLink).toHaveText('Utilisateurs'); await expect(activeLink).toHaveText('Utilisateurs');
}); });
@@ -128,11 +129,13 @@ test.describe('Admin Responsive Navigation', () => {
await page.goto(`${ALPHA_URL}/admin/users`); await page.goto(`${ALPHA_URL}/admin/users`);
await page.waitForLoadState('networkidle'); 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(); await page.getByRole('button', { name: /ouvrir le menu/i }).click();
const drawer = page.locator('[role="dialog"][aria-modal="true"]'); const drawer = page.locator('[role="dialog"][aria-modal="true"]');
await expect(drawer).toBeVisible(); await expect(drawer).toBeVisible();
// Expand "Organisation" accordion
await drawer.getByRole('button', { name: 'Organisation' }).click();
await drawer.getByRole('link', { name: 'Classes' }).click(); await drawer.getByRole('link', { name: 'Classes' }).click();
// Menu should close and page should navigate // Menu should close and page should navigate
@@ -143,6 +146,30 @@ test.describe('Admin Responsive Navigation', () => {
const label = page.locator('.mobile-section-label'); const label = page.locator('.mobile-section-label');
await expect(label).toHaveText('Classes'); 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(); 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 loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/users`); await page.goto(`${ALPHA_URL}/admin/users`);
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
@@ -172,8 +199,11 @@ test.describe('Admin Responsive Navigation', () => {
const drawer = page.locator('[role="dialog"][aria-modal="true"]'); const drawer = page.locator('[role="dialog"][aria-modal="true"]');
await expect(drawer).toBeVisible(); 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(); 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: 'Classes' })).toBeVisible();
await expect(drawer.getByRole('link', { name: 'Matières' })).toBeVisible(); await expect(drawer.getByRole('link', { name: 'Matières' })).toBeVisible();
}); });
@@ -197,18 +227,42 @@ test.describe('Admin Responsive Navigation', () => {
await expect(desktopNav).toBeVisible(); 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 loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/users`); await page.goto(`${ALPHA_URL}/admin/users`);
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
const nav = page.locator('.desktop-nav'); const nav = page.locator('.desktop-nav');
await expect(nav.getByRole('link', { name: 'Utilisateurs' })).toBeVisible();
await expect(nav.getByRole('link', { name: 'Classes' })).toBeVisible(); // Category triggers should be visible
await expect(nav.getByRole('link', { name: 'Matières' })).toBeVisible(); await expect(nav.getByRole('button', { name: /personnes/i })).toBeVisible();
await expect(nav.getByRole('link', { name: 'Affectations' })).toBeVisible(); await expect(nav.getByRole('button', { name: /organisation/i })).toBeVisible();
await expect(nav.getByRole('link', { name: 'Périodes' })).toBeVisible(); await expect(nav.getByRole('button', { name: /année scolaire/i })).toBeVisible();
await expect(nav.getByRole('link', { name: 'Pédagogie' })).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 }) => { test('hides mobile section label', async ({ page }) => {

View File

@@ -83,7 +83,10 @@ test.describe('Calendar Management (Story 2.11)', () => {
await loginAsAdmin(page); await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/classes`); 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(page).toHaveURL(/\/admin\/calendar/);
await expect( await expect(

View File

@@ -59,7 +59,10 @@ test.describe('Pedagogy - Grading Mode Configuration (Story 2.4)', () => {
await loginAsAdmin(page); await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/classes`); 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(); await expect(pedagogyLink).toBeVisible();
}); });
@@ -67,7 +70,10 @@ test.describe('Pedagogy - Grading Mode Configuration (Story 2.4)', () => {
await loginAsAdmin(page); await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/classes`); 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 }); await expect(page).toHaveURL(/\/admin\/pedagogy/, { timeout: 10000 });
}); });

View File

@@ -288,8 +288,10 @@ test.describe('Periods Management (Story 2.3)', () => {
await loginAsAdmin(page); await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin`); await page.goto(`${ALPHA_URL}/admin`);
// Click on periods link in the admin navigation // Hover "Année scolaire" category to reveal dropdown
await page.getByRole('link', { name: /périodes/i }).click(); 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).toHaveURL(/\/admin\/academic-year\/periods/);
await expect(page.getByRole('heading', { name: /périodes scolaires/i })).toBeVisible(); await expect(page.getByRole('heading', { name: /périodes scolaires/i })).toBeVisible();

View File

@@ -184,10 +184,14 @@ test.describe('Role-Based Access Control [P0]', () => {
await loginAs(page, ADMIN_EMAIL, ADMIN_PASSWORD); await loginAs(page, ADMIN_EMAIL, ADMIN_PASSWORD);
await page.goto(`${ALPHA_URL}/admin`); 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'); const nav = page.locator('.desktop-nav');
await expect(nav.getByRole('link', { name: 'Utilisateurs' })).toBeVisible({ timeout: 15000 }); await expect(nav.getByRole('button', { name: /personnes/i })).toBeVisible({ timeout: 15000 });
await expect(nav.getByRole('link', { name: 'Classes' })).toBeVisible(); 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 }) => { test('[P0] teacher sees dashboard without admin navigation', async ({ page }) => {

View File

@@ -145,8 +145,10 @@ test.describe('Student Creation & Management (Story 3.0)', () => {
await loginAsAdmin(page); await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/students`); await page.goto(`${ALPHA_URL}/admin/students`);
// The nav should have an active "Élèves" link // Hover "Personnes" category to reveal dropdown with "Élèves" link
await expect(page.locator('nav a', { hasText: /élèves/i })).toBeVisible({ 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 timeout: 10000
}); });
}); });

View File

@@ -118,7 +118,10 @@ test.describe('Teacher Assignments (Story 2.8)', () => {
await loginAsAdmin(page); await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin`); 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 }); await expect(navLink).toBeVisible({ timeout: 15000 });
}); });

View File

@@ -121,7 +121,10 @@ test.describe('Teacher Replacements (Story 2.9)', () => {
await loginAsAdmin(page); await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin`); 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 }); await expect(navLink).toBeVisible({ timeout: 15000 });
}); });

View File

@@ -22,20 +22,53 @@
'ROLE_SECRETARIAT' 'ROLE_SECRETARIAT'
]; ];
const navLinks = [ // Types
{ href: '/dashboard', label: 'Tableau de bord', isActive: () => false }, type NavLink = { href: string; label: string };
{ href: '/admin/users', label: 'Utilisateurs', isActive: () => isUsersActive }, type NavCategory = { id: string; label: string; links: NavLink[] };
{ href: '/admin/parent-invitations', label: 'Invitations parents', isActive: () => isParentInvitationsActive }, type NavEntry = NavLink | NavCategory;
{ href: '/admin/students', label: 'Élèves', isActive: () => isStudentsActive },
{ href: '/admin/classes', label: 'Classes', isActive: () => isClassesActive }, function isCategory(entry: NavEntry): entry is NavCategory {
{ href: '/admin/subjects', label: 'Matières', isActive: () => isSubjectsActive }, return 'links' in entry;
{ href: '/admin/assignments', label: 'Affectations', isActive: () => isAssignmentsActive }, }
{ href: '/admin/replacements', label: 'Remplacements', isActive: () => isReplacementsActive },
{ href: '/admin/academic-year/periods', label: 'Périodes', isActive: () => isPeriodsActive }, const navEntries: NavEntry[] = [
{ href: '/admin/calendar', label: 'Calendrier', isActive: () => isCalendarActive }, { href: '/dashboard', label: 'Tableau de bord' },
{ href: '/admin/image-rights', label: 'Droit à l\'image', isActive: () => isImageRightsActive }, {
{ href: '/admin/pedagogy', label: 'Pédagogie', isActive: () => isPedagogyActive }, id: 'personnes',
{ href: '/admin/branding', label: 'Identité visuelle', isActive: () => isBrandingActive } 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 // Load user roles and verify admin access
@@ -81,43 +114,81 @@
mobileMenuOpen = false; mobileMenuOpen = false;
} }
// Determine which admin section is active // Active link detection
const isUsersActive = $derived(page.url.pathname.startsWith('/admin/users')); const activeLinkHref = $derived.by(() => {
const isParentInvitationsActive = $derived(page.url.pathname.startsWith('/admin/parent-invitations')); const path = page.url.pathname;
const isStudentsActive = $derived(page.url.pathname.startsWith('/admin/students')); for (const entry of navEntries) {
const isClassesActive = $derived(page.url.pathname.startsWith('/admin/classes')); if (isCategory(entry)) {
const isSubjectsActive = $derived(page.url.pathname.startsWith('/admin/subjects')); for (const link of entry.links) {
const isPeriodsActive = $derived(page.url.pathname.startsWith('/admin/academic-year/periods')); if (path.startsWith(link.href)) return link.href;
const isAssignmentsActive = $derived(page.url.pathname.startsWith('/admin/assignments')); }
const isReplacementsActive = $derived(page.url.pathname.startsWith('/admin/replacements')); } else if (entry.href !== '/dashboard' && path.startsWith(entry.href)) {
const isCalendarActive = $derived(page.url.pathname.startsWith('/admin/calendar')); return entry.href;
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')); 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 currentSectionLabel = $derived.by(() => {
const path = page.url.pathname; const path = page.url.pathname;
for (const link of navLinks) { for (const entry of navEntries) {
if (link.href !== '/dashboard' && path.startsWith(link.href)) { if (isCategory(entry)) {
return link.label; for (const link of entry.links) {
if (path.startsWith(link.href)) return link.label;
}
} }
} }
return 'Administration'; 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(() => { $effect(() => {
void page.url.pathname; void page.url.pathname;
mobileMenuOpen = false; mobileMenuOpen = false;
openDesktopCategory = null;
}); });
// Close menu on Escape key // Close on Escape key
$effect(() => { $effect(() => {
if (!mobileMenuOpen) return; if (!mobileMenuOpen && !openDesktopCategory) return;
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') { if (e.key === 'Escape') {
mobileMenuOpen = false; if (openDesktopCategory) openDesktopCategory = null;
if (mobileMenuOpen) mobileMenuOpen = false;
} }
} }
@@ -125,7 +196,7 @@
return () => document.removeEventListener('keydown', handleKeydown); return () => document.removeEventListener('keydown', handleKeydown);
}); });
// Lock body scroll when menu is open // Lock body scroll when mobile menu is open
$effect(() => { $effect(() => {
if (mobileMenuOpen) { if (mobileMenuOpen) {
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
@@ -169,10 +240,52 @@
<nav class="desktop-nav"> <nav class="desktop-nav">
<RoleSwitcher /> <RoleSwitcher />
{#each navLinks as link} {#each navEntries as entry}
<a href={link.href} class="nav-link" class:active={link.isActive()}>{link.label}</a> {#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}
>
&#9662;
</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}
</div>
{/if}
</div>
{:else}
<a href={entry.href} class="nav-link">{entry.label}</a>
{/if}
{/each} {/each}
<button class="nav-button" onclick={goSettings}>Paramètres</button> <button class="nav-button" onclick={goSettings}>Réglages</button>
<button class="logout-button" onclick={handleLogout} disabled={isLoggingOut}> <button class="logout-button" onclick={handleLogout} disabled={isLoggingOut}>
{#if isLoggingOut} {#if isLoggingOut}
<span class="spinner"></span> <span class="spinner"></span>
@@ -215,18 +328,49 @@
<div class="mobile-role-switcher"> <div class="mobile-role-switcher">
<RoleSwitcher /> <RoleSwitcher />
</div> </div>
{#each navLinks as link} {#each navEntries as entry}
<a {#if isCategory(entry)}
href={link.href} <div class="mobile-section">
class="mobile-nav-link" <button
class:active={link.isActive()} class="mobile-section-toggle"
> class:active={activeCategoryId === entry.id}
{link.label} aria-expanded={openMobileCategories.has(entry.id)}
</a> onclick={() => toggleMobileCategory(entry.id)}
>
{entry.label}
<span
class="accordion-chevron"
class:open={openMobileCategories.has(entry.id)}
>
&#9662;
</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={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} {/each}
</div> </div>
<div class="mobile-drawer-footer"> <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 <button
class="mobile-nav-link mobile-logout" class="mobile-nav-link mobile-logout"
onclick={handleLogout} onclick={handleLogout}
@@ -378,6 +522,65 @@
background: var(--accent-primary-light, #e0f2fe); 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 { .nav-button {
padding: 0.375rem 0.625rem; padding: 0.375rem 0.625rem;
font-size: 0.8125rem; font-size: 0.8125rem;
@@ -486,6 +689,63 @@
margin-bottom: 0.5rem; 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 { .mobile-nav-link {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -527,13 +787,21 @@
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
@keyframes slideInLeft { @keyframes slideInLeft {
from { transform: translateX(-100%); } from {
to { transform: translateX(0); } transform: translateX(-100%);
}
to {
transform: translateX(0);
}
} }
.spinner { .spinner {