SPB Git forge

spb/ka-ui

Public
30commits 1branches 0releases
145.7 MBsize
maindefault branch
27 days agolast push
Python 33.5% JavaScript 30.1% TypeScript 25% CSS 10% Shell 1.4%

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 1 mo ago (Aug 25, 2026) parent 79fb931

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