389 lines
12 KiB
Markdown
389 lines
12 KiB
Markdown
# 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
|
|
<?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` :
|
|
|
|
```css
|
|
/* 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
|
|
|
|
```javascript
|
|
// 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
|
|
<?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_
|