SPB Git

spb/ka-ui

Public

nav/ : standard KA Nav v2 — menu mobile plein écran fixed (2026-08-25)

NAV-STANDARD.md (règles), ka-nav.css + ka-nav.js (référence vanilla à adapter
par app), convert-mobile-menu.py (conversion des .mobile-menu dropdown des
apps React). Déployé ce jour sur les 14 sites publics du groupe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 25, 2026) parent 79fb931 Browse files

4 changed files +176 −0

added nav/NAV-STANDARD.md +35 −0
@@ -0,0 +1,35 @@
1+# Standard KA Nav v2 — menu mobile plein écran (2026-08-25)
2+
3+## Problème corrigé
4+Sur mobile, plusieurs sites KA affichaient soit des liens de nav qui wrappent dans le
5+header (2-3 lignes), soit un dropdown `position:absolute` ancré au header. Quand le
6+header n'est pas sticky (ou que le panneau est ancré au flux du document), ouvrir le
7+menu en bas de page ne montre RIEN — le panneau se dessine hors écran. Rendu incohérent
8+d'un site à l'autre.
9+
10+## Standard (obligatoire sur tous les sites publics KA)
11+- Desktop (>760px) : nav pilules inline inchangée.
12+- Mobile (≤760px) : les liens inline sont masqués, remplacés par un bouton hamburger
13+ 44×44 (3 barres → ✕ animé, `aria-expanded`).
14+- Panneau : `position:fixed; inset:0; z-index:var(--z-modal,900); background:var(--paper)`
15+ → TOUJOURS visible, peu importe la position de scroll (c'est le fix du bug).
16+ Structure : barre du haut (marque + bouton ✕ 44px) / liens géants numérotés
17+ (font-display 700, min-height 52px, bordure pointillée) / pied (badge Groupe KA +
18+ liens secondaires), `padding-bottom:max(20px, env(safe-area-inset-bottom))`.
19+- Verrou de scroll : classe `ka-menu-locked` sur `<html>` (`overflow:hidden`).
20+- Fermeture : clic sur un lien, ✕, touche Escape. Le panneau s'ouvre sans animation
21+ lourde (fade+translate 160ms max).
22+- A11y : `role="dialog" aria-modal="true"`, `aria-label="Menu"`, focus visible.
23+- Thème automatique par app via tokens : `--paper, --ink, --ink-2, --ink-3, --accent,
24+ --on-accent, --accent-soft, --font-display, --font-mono, --z-modal`.
25+- Référence Next.js : vrai-prix `src/components/HeaderNav.tsx` (checkbox + peer-checked).
26+- Référence vanilla SPA : `ka-nav.css` + `ka-nav.js` de ce dossier (copiés/adaptés
27+ dans chaque index.html monolithique).
28+
29+## Règles
30+- JAMAIS de dropdown `position:absolute` ancré au header pour le menu mobile.
31+- Le hamburger et le ✕ font ≥44×44px (cible tactile).
32+- Les liens du panneau reprennent EXACTEMENT les liens de la nav desktop (+ liens
33+ secondaires du footer si pertinent), i18n fr-CA.
34+- L'état actif (page courante) est marqué dans le panneau (fond --ink, texte --accent
35+ ou inversion propre au thème).
added nav/convert-mobile-menu.py +62 −0
@@ -0,0 +1,62 @@
1+#!/usr/bin/env python3
2+"""KA Nav v2 (2026-08-25) — convertit le .mobile-menu dropdown absolute 480px
3+en panneau plein écran position:fixed inset:0 dans un styles.css d'app KA.
4+Usage : convert-mobile-menu.py <styles.css> <has|menu-open>
5+ has → le header est remonté via .header:has(.mobile-menu.open)
6+ menu-open → le header est remonté via .header.menu-open
7+Affiche n1/n2/n3 (chaque compteur doit être ≥1, sauf n3 qui peut être 0)."""
8+import re
9+import sys
10+
11+path, variant = sys.argv[1], sys.argv[2]
12+css = open(path).read()
13+
14+# 1) bloc principal : absolute top:100% + max-height:0 → fixed inset:0
15+pat = re.compile(
16+ r"\.mobile-menu \{\s*\n"
17+ r"(?:[^}]*?\n)*?"
18+ r"\s*transition: max-height[^\n]*\n"
19+ r"\}"
20+)
21+new_block = (
22+ "/* KA Nav v2 (2026-08-25) : panneau PLEIN ECRAN fixed inset:0 - visible peu\n"
23+ " importe le scroll (l'ancien dropdown absolute 480px clippait/se coupait). */\n"
24+ ".mobile-menu {\n"
25+ " display: none; position: fixed; inset: 0;\n"
26+ " background: var(--paper);\n"
27+ " padding: 76px 16px calc(24px + env(safe-area-inset-bottom));\n"
28+ " overflow-y: auto; overscroll-behavior: contain;\n"
29+ "}"
30+)
31+css, n1 = pat.subn(new_block, css, count=1)
32+if n1 == 0: # variante mono-ligne (immo-ka)
33+ pat1b = re.compile(r"\.mobile-menu \{ display: none; position: absolute;[^}]*\}")
34+ css, n1 = pat1b.subn(new_block, css, count=1)
35+
36+# 2) l'ancienne ligne z-index dediee (lou-ka / resto-ka) devient inutile :
37+# le panneau doit peindre SOUS le rang du header (header-inner z:60)
38+css = css.replace(".mobile-menu { z-index: var(--z-modal, 900); }\n", "")
39+
40+# 3) regle d'ouverture + rang du header au-dessus + verrou de scroll CSS pur
41+raise_rule = (
42+ ".header:has(.mobile-menu.open) .header-inner { position: relative; z-index: 60; }"
43+ if variant == "has"
44+ else ".header.menu-open .header-inner { position: relative; z-index: 60; }"
45+)
46+open_block = (
47+ ".mobile-menu.open { display: block; animation: mm-in 0.16s ease; }\n"
48+ "@keyframes mm-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }\n"
49+ "/* le rang du header (marque + burger devenu X) reste au-dessus du panneau */\n"
50+ + raise_rule + "\n"
51+ "/* verrou du scroll d'arriere-plan quand le menu est ouvert (CSS pur) */\n"
52+ "html:has(.mobile-menu.open) { overflow: hidden; }"
53+)
54+pat2 = re.compile(r"\.mobile-menu\.open \{ max-height: 480px; opacity: 1;(?: overflow-y: auto;)? \}")
55+css, n2 = pat2.subn(open_block, css, count=1)
56+
57+# 4) dans la media query mobile, le display:block permanent disparait
58+# (le display est desormais porte par .open)
59+css, n3 = re.subn(r"\s*\.mobile-menu \{ display: block; \}", "", css)
60+
61+open(path, "w").write(css)
62+print(f"n1={n1} n2={n2} n3={n3}")
added nav/ka-nav.css +57 −0
@@ -0,0 +1,57 @@
1+/* ===== KA Nav v2 — menu mobile plein écran (standard 2026-08-25) =====
2+ Panneau en position:fixed inset:0 → toujours visible, peu importe le scroll.
3+ Thémé par les tokens de l'app : --paper --ink --ink-2 --ink-3 --accent
4+ --on-accent --font-display --font-mono --z-modal. Préfixe ka-mnav (zéro collision). */
5+
6+/* bouton hamburger (masqué en desktop) */
7+.ka-mnav-btn{display:none;width:44px;height:44px;margin-left:auto;flex:none;
8+ flex-direction:column;align-items:center;justify-content:center;gap:5px;
9+ background:var(--paper);border:1.5px solid var(--ink);border-radius:999px;
10+ cursor:pointer;padding:0}
11+.ka-mnav-btn span{display:block;width:18px;height:2px;background:var(--ink);
12+ border-radius:2px;transition:transform .18s ease,opacity .14s ease}
13+.ka-mnav-btn[aria-expanded="true"] span:nth-child(1){transform:translateY(7px) rotate(45deg)}
14+.ka-mnav-btn[aria-expanded="true"] span:nth-child(2){opacity:0}
15+.ka-mnav-btn[aria-expanded="true"] span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}
16+
17+/* panneau plein écran */
18+.ka-mnav{position:fixed;inset:0;z-index:var(--z-modal,900);background:var(--paper);
19+ display:none;flex-direction:column;overflow-y:auto;overscroll-behavior:contain}
20+.ka-mnav.open{display:flex;animation:ka-mnav-in .16s ease}
21+@keyframes ka-mnav-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
22+
23+.ka-mnav-top{display:flex;align-items:center;justify-content:space-between;flex:none;
24+ min-height:64px;padding:0 18px;border-bottom:2px solid var(--ink);background:var(--paper)}
25+.ka-mnav-close{width:44px;height:44px;flex:none;display:flex;align-items:center;
26+ justify-content:center;border:1.5px solid var(--ink);border-radius:999px;
27+ background:var(--ink);color:var(--accent,var(--paper));font-size:17px;font-weight:700;
28+ font-family:var(--font-display,inherit);cursor:pointer;padding:0;line-height:1}
29+
30+.ka-mnav-links{display:flex;flex-direction:column;justify-content:center;flex:1;
31+ padding:28px 24px}
32+.ka-mnav-links a{display:flex;align-items:baseline;gap:14px;padding:17px 0;
33+ border-bottom:1.5px dashed color-mix(in srgb,var(--ink) 18%,transparent);
34+ text-decoration:none;color:var(--ink)}
35+.ka-mnav-links a:last-child{border-bottom:0}
36+.ka-mnav-links a i{font-style:normal;font-family:var(--font-mono,monospace);
37+ font-size:12px;font-weight:700;color:var(--accent-deep,var(--ink-3,#777))}
38+.ka-mnav-links a b{font-family:var(--font-display,inherit);font-weight:700;
39+ font-size:27px;line-height:1;letter-spacing:-.03em;text-transform:uppercase}
40+.ka-mnav-links a.active b{background:var(--ink);color:var(--accent,var(--paper));
41+ border-radius:6px;padding:4px 10px 6px}
42+
43+.ka-mnav-foot{flex:none;border-top:2px solid var(--ink);padding:16px 24px;
44+ padding-bottom:max(20px,env(safe-area-inset-bottom))}
45+.ka-mnav-foot .ka-mnav-sec{display:flex;flex-wrap:wrap;gap:6px 20px;margin-top:12px;
46+ font-family:var(--font-mono,monospace);font-size:10.5px;text-transform:uppercase;
47+ letter-spacing:.06em}
48+.ka-mnav-foot .ka-mnav-sec a{color:var(--ink-3,var(--ink));text-decoration:none}
49+
50+/* verrou de scroll quand le panneau est ouvert */
51+html.ka-menu-locked,html.ka-menu-locked body{overflow:hidden}
52+
53+/* bascule mobile : masquer la nav inline, montrer le hamburger.
54+ ⚠️ PAR APP : remplacer `.nav` par le sélecteur réel de la nav desktop. */
55+@media(max-width:760px){
56+ .ka-mnav-btn{display:flex}
57+}
added nav/ka-nav.js +22 −0
@@ -0,0 +1,22 @@
1+/* ===== KA Nav v2 — comportement du menu mobile plein écran (2026-08-25) =====
2+ Ouvre/ferme le panneau #ka-mnav via #ka-mnav-btn ; verrou de scroll sur <html> ;
3+ fermeture au clic sur un lien, sur ✕ (data-ka-mnav-close) ou avec Escape. */
4+(function () {
5+ var btn = document.getElementById("ka-mnav-btn");
6+ var panel = document.getElementById("ka-mnav");
7+ if (!btn || !panel) return;
8+ function set(open) {
9+ panel.classList.toggle("open", open);
10+ btn.setAttribute("aria-expanded", open ? "true" : "false");
11+ document.documentElement.classList.toggle("ka-menu-locked", open);
12+ }
13+ btn.addEventListener("click", function () {
14+ set(!panel.classList.contains("open"));
15+ });
16+ panel.addEventListener("click", function (e) {
17+ if (e.target.closest("a") || e.target.closest("[data-ka-mnav-close]")) set(false);
18+ });
19+ document.addEventListener("keydown", function (e) {
20+ if (e.key === "Escape") set(false);
21+ });
22+})();
23
Menu

Theme