Files
Portfolio-Codex/docs/stories/2.1.navbar-responsive.md

12 KiB

Story 2.1: Navbar Responsive avec Menu Mobile

Status

Ready for Dev

Story

As a visiteur, I want disposer d'une navigation claire et accessible sur tous les appareils, so that je trouve facilement les sections du portfolio.

Acceptance Criteria

  1. templates/navbar.php contient le menu de navigation avec les liens : Accueil, Projets, Compétences, Me Découvrir, Contact
  2. La navbar est fixe en haut de page (sticky) et reste visible au scroll
  3. Sur mobile, un menu "hamburger" affiche/masque les liens en JavaScript vanilla
  4. Le lien de la page active est visuellement distingué (couleur/soulignement)
  5. La navbar est responsive et s'adapte aux 3 breakpoints (mobile, tablette, desktop)
  6. La navbar a un effet d'ombre au scroll (optionnel mais recommandé)

Tasks / Subtasks

  • [] Task 1 : Créer le template navbar.php (AC: 1, 2)

    • [] Créer templates/navbar.php
    • [] Ajouter la structure HTML sémantique (<header>, <nav>)
    • [] Ajouter le logo/nom du site (lien vers accueil)
    • [] Ajouter les liens de navigation desktop
    • [] Appliquer les classes Tailwind pour sticky + fond
  • [] Task 2 : Implémenter le menu mobile hamburger (AC: 3)

    • [] Ajouter le bouton hamburger (icône 3 barres)
    • [] Ajouter le menu mobile (overlay ou slide)
    • [] Masquer le bouton sur desktop, afficher sur mobile
    • [] Ajouter l'attribut aria-expanded pour l'accessibilité
  • [] Task 3 : Créer le JavaScript pour le menu mobile (AC: 3)

    • [] Créer ou mettre à jour assets/js/main.js
    • [] Implémenter le toggle du menu (ouvert/fermé)
    • [] Fermer le menu au clic sur un lien
    • [] Fermer le menu avec la touche Escape
    • [] Gérer l'état aria-expanded
  • [] Task 4 : Implémenter l'état actif des liens (AC: 4)

    • [] Passer la page courante via une variable PHP
    • [] Appliquer une classe visuelle sur le lien actif
    • [] Utiliser la couleur primary ou un soulignement
  • [] Task 5 : Rendre la navbar responsive (AC: 5)

    • [] Mobile : logo + hamburger uniquement
    • [] Tablette/Desktop : tous les liens visibles
    • [] Vérifier les 3 breakpoints
  • [] Task 6 : Ajouter l'effet au scroll (AC: 6)

    • [] Ajouter une ombre quand la page est scrollée
    • [] Utiliser JavaScript pour détecter le scroll
    • [] Transition smooth pour l'effet
  • [] Task 7 : Intégrer la navbar dans les pages

    • [] Modifier header.php ou les pages pour inclure la navbar
    • [] Passer la variable $currentPage à la navbar

Dev Notes

Structure HTML de la Navbar

<?php
/**
 * Template Navbar
 * Variables disponibles :
 * - $currentPage (string) : identifiant de la page active ('home', 'projects', 'skills', 'about', 'contact')
 */

$currentPage = $currentPage ?? 'home';

$navLinks = [
    ['id' => 'home', 'label' => 'Accueil', 'url' => '/'],
    ['id' => 'projects', 'label' => 'Projets', 'url' => '/projets'],
    ['id' => 'skills', 'label' => 'Compétences', 'url' => '/competences'],
    ['id' => 'about', 'label' => 'Me Découvrir', 'url' => '/a-propos'],
    ['id' => 'contact', 'label' => 'Contact', 'url' => '/contact', 'isCta' => true],
];
?>

<header id="navbar" class="fixed top-0 left-0 right-0 z-50 bg-background/95 backdrop-blur-sm border-b border-border/50 transition-shadow duration-300">
    <nav class="container-content" aria-label="Navigation principale">
        <div class="flex items-center justify-between h-16 lg:h-20">

            <!-- Logo -->
            <a href="/" class="text-xl font-bold text-text-primary hover:text-primary transition-colors">
                Portfolio
            </a>

            <!-- Navigation Desktop -->
            <ul class="hidden lg:flex items-center gap-1">
                <?php foreach ($navLinks as $link): ?>
                    <?php if (!empty($link['isCta'])): ?>
                        <li class="ml-4">
                            <a href="<?= $link['url'] ?>" class="btn-primary text-sm">
                                <?= $link['label'] ?>
                            </a>
                        </li>
                    <?php else: ?>
                        <li>
                            <a href="<?= $link['url'] ?>"
                               class="nav-link <?= $currentPage === $link['id'] ? 'nav-link-active' : '' ?>">
                                <?= $link['label'] ?>
                            </a>
                        </li>
                    <?php endif; ?>
                <?php endforeach; ?>
            </ul>

            <!-- Bouton Hamburger Mobile -->
            <button
                id="mobile-menu-toggle"
                class="lg:hidden p-2 text-text-primary hover:text-primary transition-colors"
                aria-label="Ouvrir le menu"
                aria-expanded="false"
                aria-controls="mobile-menu"
            >
                <!-- Icône Hamburger -->
                <svg class="w-6 h-6 hamburger-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
                </svg>
                <!-- Icône Close (hidden par défaut) -->
                <svg class="w-6 h-6 close-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
                </svg>
            </button>
        </div>

        <!-- Menu Mobile -->
        <div id="mobile-menu" class="lg:hidden hidden" aria-hidden="true">
            <ul class="py-4 space-y-2 border-t border-border">
                <?php foreach ($navLinks as $link): ?>
                    <li>
                        <a href="<?= $link['url'] ?>"
                           class="block py-3 px-4 rounded-lg <?= $currentPage === $link['id'] ? 'bg-surface text-primary' : 'text-text-secondary hover:bg-surface hover:text-text-primary' ?> transition-colors">
                            <?= $link['label'] ?>
                        </a>
                    </li>
                <?php endforeach; ?>
            </ul>
        </div>
    </nav>
</header>

<!-- Spacer pour compenser la navbar fixed -->
<div class="h-16 lg:h-20"></div>

Classes CSS Additionnelles (input.css)

Ajouter dans @layer components :

/* Navigation links */
.nav-link {
    @apply px-4 py-2 text-sm font-medium text-text-secondary
           hover:text-text-primary transition-colors rounded-lg
           hover:bg-surface-light;
}

.nav-link-active {
    @apply text-primary bg-primary/10;
}

JavaScript pour le Menu Mobile

// assets/js/main.js

document.addEventListener('DOMContentLoaded', () => {
    initMobileMenu();
    initNavbarScroll();
});

/**
 * Gestion du menu mobile
 */
function initMobileMenu() {
    const toggle = document.getElementById('mobile-menu-toggle');
    const menu = document.getElementById('mobile-menu');

    if (!toggle || !menu) return;

    const hamburgerIcon = toggle.querySelector('.hamburger-icon');
    const closeIcon = toggle.querySelector('.close-icon');

    function openMenu() {
        menu.classList.remove('hidden');
        menu.setAttribute('aria-hidden', 'false');
        toggle.setAttribute('aria-expanded', 'true');
        toggle.setAttribute('aria-label', 'Fermer le menu');
        hamburgerIcon?.classList.add('hidden');
        closeIcon?.classList.remove('hidden');
    }

    function closeMenu() {
        menu.classList.add('hidden');
        menu.setAttribute('aria-hidden', 'true');
        toggle.setAttribute('aria-expanded', 'false');
        toggle.setAttribute('aria-label', 'Ouvrir le menu');
        hamburgerIcon?.classList.remove('hidden');
        closeIcon?.classList.add('hidden');
    }

    function toggleMenu() {
        const isOpen = toggle.getAttribute('aria-expanded') === 'true';
        if (isOpen) {
            closeMenu();
        } else {
            openMenu();
        }
    }

    // Toggle au clic
    toggle.addEventListener('click', toggleMenu);

    // Fermer au clic sur un lien
    menu.querySelectorAll('a').forEach(link => {
        link.addEventListener('click', closeMenu);
    });

    // Fermer avec Escape
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape' && toggle.getAttribute('aria-expanded') === 'true') {
            closeMenu();
            toggle.focus();
        }
    });

    // Fermer si on redimensionne vers desktop
    window.addEventListener('resize', () => {
        if (window.innerWidth >= 1024) {
            closeMenu();
        }
    });
}

/**
 * Effet d'ombre au scroll
 */
function initNavbarScroll() {
    const navbar = document.getElementById('navbar');
    if (!navbar) return;

    let lastScroll = 0;

    window.addEventListener('scroll', () => {
        const currentScroll = window.scrollY;

        if (currentScroll > 10) {
            navbar.classList.add('shadow-lg');
        } else {
            navbar.classList.remove('shadow-lg');
        }

        lastScroll = currentScroll;
    }, { passive: true });
}

Intégration dans les Pages

Modifier index.php et les futures pages :

<?php
require_once __DIR__ . '/includes/functions.php';

include_template('header', [
    'pageTitle' => 'Accueil',
]);

include_template('navbar', [
    'currentPage' => 'home'
]);
?>

<!-- Contenu de la page -->

<?php include_template('footer'); ?>

Structure de Navigation

Lien URL ID
Accueil / home
Projets /projets projects
Compétences /competences skills
Me Découvrir /a-propos about
Contact (CTA) /contact contact

Breakpoints Responsive

Breakpoint Comportement
Mobile (< 1024px) Logo + hamburger, menu caché
Desktop (≥ 1024px) Tous les liens visibles

Accessibilité

  • aria-label sur le bouton hamburger
  • aria-expanded pour indiquer l'état du menu
  • aria-controls pour lier le bouton au menu
  • aria-hidden sur le menu mobile
  • Navigation au clavier (Tab, Escape)
  • Focus visible sur tous les éléments interactifs

Testing

Tests Fonctionnels

  • Tous les liens sont présents et cliquables
  • La navbar reste visible au scroll (sticky)
  • Le lien actif est visuellement distinct
  • L'ombre apparaît au scroll

Tests Mobile

  • Le bouton hamburger est visible sur mobile
  • Le menu s'ouvre au clic
  • Le menu se ferme au clic sur un lien
  • Le menu se ferme avec Escape
  • L'icône change (hamburger ↔ X)

Tests Accessibilité

  • Navigation complète au clavier
  • aria-expanded se met à jour
  • Focus visible sur tous les éléments
  • Lecteur d'écran annonce l'état du menu

Tests Responsive

Mobile (375px)  → hamburger visible, liens cachés
Tablet (768px)  → hamburger visible, liens cachés
Desktop (1024px) → liens visibles, hamburger caché
Wide (1440px)   → liens visibles, centré

Change Log

Date Version Description Author
2026-01-22 0.1 Création initiale de la story Sarah (PO)

Dev Agent Record

Agent Model Used

Claude Opus 4.5 (claude-opus-4-5-20251101)

Debug Log References

À compléter par le dev agent

Completion Notes List

  • templates/navbar.php créé avec structure sémantique complète
  • Menu hamburger avec icônes SVG (hamburger/close)
  • JavaScript vanilla pour toggle menu, fermeture Escape, resize
  • Effet d'ombre au scroll (shadow-lg)
  • Classes nav-link et nav-link-active ajoutées à input.css
  • État actif via variable $currentPage
  • Accessibilité complète (aria-expanded, aria-controls, aria-hidden)
  • Spacer pour compenser navbar fixed
  • CSS regénéré (15 Ko)

File List

Fichier Action
templates/navbar.php Créé
assets/css/input.css Modifié (classes nav-link)
assets/js/main.js Modifié (menu mobile + scroll)
index.php Modifié (intégration navbar)
assets/css/output.css Regénéré

QA Results

À compléter par le QA agent