✨ Story 2.1: navbar responsive
This commit is contained in:
@@ -152,6 +152,17 @@
|
||||
.breadcrumb-current {
|
||||
@apply text-text-primary;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@@ -187,4 +198,4 @@
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,5 +2,75 @@
|
||||
// Script principal du portfolio
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('Portfolio chargé');
|
||||
});
|
||||
initMobileMenu();
|
||||
initNavbarScroll();
|
||||
});
|
||||
|
||||
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.addEventListener('click', toggleMenu);
|
||||
|
||||
menu.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', closeMenu);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && toggle.getAttribute('aria-expanded') === 'true') {
|
||||
closeMenu();
|
||||
toggle.focus();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initNavbarScroll() {
|
||||
const navbar = document.getElementById('navbar');
|
||||
if (!navbar) return;
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 10) {
|
||||
navbar.classList.add('shadow-lg');
|
||||
} else {
|
||||
navbar.classList.remove('shadow-lg');
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
review
|
||||
|
||||
## Story
|
||||
|
||||
@@ -21,44 +21,44 @@ Ready for Dev
|
||||
|
||||
## 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
|
||||
- [x] **Task 1 : Créer le template navbar.php** (AC: 1, 2)
|
||||
- [x] Créer `templates/navbar.php`
|
||||
- [x] Ajouter la structure HTML sémantique (`<header>`, `<nav>`)
|
||||
- [x] Ajouter le logo/nom du site (lien vers accueil)
|
||||
- [x] Ajouter les liens de navigation desktop
|
||||
- [x] 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é
|
||||
- [x] **Task 2 : Implémenter le menu mobile hamburger** (AC: 3)
|
||||
- [x] Ajouter le bouton hamburger (icône 3 barres)
|
||||
- [x] Ajouter le menu mobile (overlay ou slide)
|
||||
- [x] Masquer le bouton sur desktop, afficher sur mobile
|
||||
- [x] 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`
|
||||
- [x] **Task 3 : Créer le JavaScript pour le menu mobile** (AC: 3)
|
||||
- [x] Créer ou mettre à jour `assets/js/main.js`
|
||||
- [x] Implémenter le toggle du menu (ouvert/fermé)
|
||||
- [x] Fermer le menu au clic sur un lien
|
||||
- [x] Fermer le menu avec la touche Escape
|
||||
- [x] 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
|
||||
- [x] **Task 4 : Implémenter l'état actif des liens** (AC: 4)
|
||||
- [x] Passer la page courante via une variable PHP
|
||||
- [x] Appliquer une classe visuelle sur le lien actif
|
||||
- [x] 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
|
||||
- [x] **Task 5 : Rendre la navbar responsive** (AC: 5)
|
||||
- [x] Mobile : logo + hamburger uniquement
|
||||
- [x] Tablette/Desktop : tous les liens visibles
|
||||
- [x] 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
|
||||
- [x] **Task 6 : Ajouter l'effet au scroll** (AC: 6)
|
||||
- [x] Ajouter une ombre quand la page est scrollée
|
||||
- [x] Utiliser JavaScript pour détecter le scroll
|
||||
- [x] 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
|
||||
- [x] **Task 7 : Intégrer la navbar dans les pages**
|
||||
- [x] Modifier `header.php` ou les pages pour inclure la navbar
|
||||
- [x] Passer la variable `$currentPage` à la navbar
|
||||
|
||||
## Dev Notes
|
||||
|
||||
@@ -349,17 +349,18 @@ Wide (1440px) → liens visibles, centré
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-02-04 | 0.1 | Implementation story 2.1 | Amelia |
|
||||
| 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)
|
||||
GPT-5 Codex
|
||||
|
||||
### Debug Log References
|
||||
|
||||
_À compléter par le dev agent_
|
||||
- tests/navbar.test.ps1: navbar coverage
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
@@ -371,7 +372,8 @@ _À compléter par le dev agent_
|
||||
- É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)
|
||||
- CSS régénéré via `npm run build`
|
||||
- Tests: `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
|
||||
|
||||
### File List
|
||||
|
||||
@@ -382,6 +384,8 @@ _À compléter par le dev agent_
|
||||
| `assets/js/main.js` | Modifié (menu mobile + scroll) |
|
||||
| `index.php` | Modifié (intégration navbar) |
|
||||
| `assets/css/output.css` | Regénéré |
|
||||
| `tests/navbar.test.ps1` | Créé |
|
||||
| `tests/run.ps1` | Modifié |
|
||||
|
||||
## QA Results
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ include_template('header', [
|
||||
'pageTitle' => 'Portfolio en construction',
|
||||
'pageDescription' => 'Mon portfolio de développeur web arrive bientôt. Restez connectés !'
|
||||
]);
|
||||
|
||||
include_template('navbar', [
|
||||
'currentPage' => 'home'
|
||||
]);
|
||||
?>
|
||||
|
||||
<main class="min-h-screen flex items-center justify-center">
|
||||
|
||||
76
templates/navbar.php
Normal file
76
templates/navbar.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Navbar
|
||||
* Variables disponibles :
|
||||
* - $currentPage (string) : identifiant de la page active
|
||||
*/
|
||||
|
||||
$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">
|
||||
<a href="/" class="text-xl font-bold text-text-primary hover:text-primary transition-colors">
|
||||
Portfolio
|
||||
</a>
|
||||
|
||||
<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>
|
||||
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="h-16 lg:h-20"></div>
|
||||
25
tests/navbar.test.ps1
Normal file
25
tests/navbar.test.ps1
Normal file
@@ -0,0 +1,25 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Assert-True {
|
||||
param(
|
||||
[bool]$Condition,
|
||||
[string]$Message
|
||||
)
|
||||
if (-not $Condition) { throw $Message }
|
||||
}
|
||||
|
||||
Assert-True (Test-Path 'templates/navbar.php') 'Missing templates/navbar.php'
|
||||
$navbar = Get-Content -Raw 'templates/navbar.php'
|
||||
Assert-True ($navbar -match 'mobile-menu-toggle') 'Navbar missing toggle id'
|
||||
Assert-True ($navbar -match 'nav-link-active') 'Navbar missing active class'
|
||||
|
||||
Assert-True (Test-Path 'assets/js/main.js') 'Missing assets/js/main.js'
|
||||
$js = Get-Content -Raw 'assets/js/main.js'
|
||||
Assert-True ($js -match 'initMobileMenu') 'Missing initMobileMenu'
|
||||
Assert-True ($js -match 'initNavbarScroll') 'Missing initNavbarScroll'
|
||||
|
||||
Assert-True (Test-Path 'index.php') 'Missing index.php'
|
||||
$index = Get-Content -Raw 'index.php'
|
||||
Assert-True ($index -match "include_template\('navbar'") 'index.php missing navbar include'
|
||||
|
||||
'OK'
|
||||
@@ -4,4 +4,5 @@ $here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
& (Join-Path $here 'tailwind.test.ps1')
|
||||
& (Join-Path $here 'templates.test.ps1')
|
||||
& (Join-Path $here 'canary.test.ps1')
|
||||
& (Join-Path $here 'navbar.test.ps1')
|
||||
'OK'
|
||||
Reference in New Issue
Block a user