12 KiB
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
templates/navbar.phpcontient le menu de navigation avec les liens : Accueil, Projets, Compétences, Me Découvrir, Contact- La navbar est fixe en haut de page (sticky) et reste visible au scroll
- Sur mobile, un menu "hamburger" affiche/masque les liens en JavaScript vanilla
- Le lien de la page active est visuellement distingué (couleur/soulignement)
- La navbar est responsive et s'adapte aux 3 breakpoints (mobile, tablette, desktop)
- 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
- [] Créer
-
[] 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-expandedpour 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
- [] Créer ou mettre à jour
-
[] 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.phpou les pages pour inclure la navbar - [] Passer la variable
$currentPageà la navbar
- [] Modifier
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-labelsur le bouton hamburgeraria-expandedpour indiquer l'état du menuaria-controlspour lier le bouton au menuaria-hiddensur 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-expandedse 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