SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
3 days agolast push
HTML 98.9% Python 0.6%

Mobile : backdrop du menu compte via createPortal, verrou scroll de la feuille filtres, champs ≥16 px, logo tactile, socle mobile tokens.css, ka-agent corrigé

- frontend/src/App.tsx : .account-backdrop rendu à la racine via createPortal
  (le backdrop-filter du header re-parentait le position:fixed → voile de
  66 px) + fermeture du menu compte à Escape.
- frontend/src/pages/Home.tsx : feuille de filtres mobile (sheetOpen) — gel du
  scroll d arrière-plan (pattern du hamburger) + fermeture à Escape.
- frontend/src/styles.css : backdrop compte plein écran sous le header (z-40,
  le menu ancré dans le header reste au-dessus) ; champs de filtres ≥16 px sur
  mobile (f-search, f-native, f-ctl select, ms-sort) ; zone tactile ≥44 px du
  logo .brand (::after inset -8px, rendu inchangé).
- frontend/src/ka/tokens.css : socle mobile obligatoire (z-index communs,
  --vh100, .ka-scroll-lock, .ka-bottombar, filet anti-zoom iOS 16 px).
- frontend/public/ka-agent.js : safe-areas, textarea 16 px, plein écran
  ≤640 px + verrou, pas de focus clavier forcé.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 19, 2026) parent 73bc289

5 changed files +153 −16

modified frontend/public/ka-agent.js +24 −12
@@ -17,19 +17,19 @@
17 17
18 18 /* ---------- styles ---------- */
19 19 var css = `
20 − .kaa-btn{position:fixed;right:18px;bottom:18px;z-index:2147483000;width:58px;height:58px;
20 + .kaa-btn{position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom,0px));z-index:2147483000;width:58px;height:58px;
21 21 border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b);
22 22 color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif;
23 23 cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center;
24 − justify-content:center;transition:transform .15s;padding-bottom:env(safe-area-inset-bottom,0)}
24 + justify-content:center;transition:transform .15s}
25 25 .kaa-btn:hover{transform:translate(-2px,-2px)}
26 − .kaa-panel{position:fixed;right:16px;bottom:86px;z-index:2147483001;width:min(392px,calc(100vw - 24px));
27 − height:min(580px,calc(100dvh - 110px));display:none;flex-direction:column;background:var(--paper,#f5f3ee);
26 + .kaa-panel{position:fixed;right:16px;bottom:calc(86px + env(safe-area-inset-bottom,0px));z-index:2147483001;width:min(392px,calc(100vw - 24px));
27 + height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px));display:none;flex-direction:column;background:var(--paper,#f5f3ee);
28 28 border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85);
29 29 overflow:hidden;font-family:Inter,system-ui,sans-serif}
30 30 .kaa-panel.kaa-open{display:flex}
31 − .kaa-panel.kaa-full{right:0;bottom:0;left:0;top:0;width:100vw;height:100dvh;border-radius:0;border:0;box-shadow:none;
32 − padding-bottom:env(safe-area-inset-bottom)}
31 + .kaa-panel.kaa-full{right:0;bottom:0;width:100vw;height:100vh;height:100dvh;border-radius:0;border:0;box-shadow:none}
32 + html.kaa-lock,html.kaa-lock body{overflow:hidden!important;overscroll-behavior:none}
33 33 .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814);
34 34 color:var(--paper,#f5f3ee);flex:none}
35 35 .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em}
@@ -54,7 +54,7 @@
54 54 border-radius:999px;background:var(--surface-2,#faf9f5)}
55 55 .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none;
56 56 padding-bottom:calc(11px + env(safe-area-inset-bottom,0))}
57 − .kaa-in textarea{flex:1;resize:none;min-height:44px;max-height:110px;padding:11px 12px;font:13.5px/1.4 Inter,system-ui,sans-serif;
57 + .kaa-in textarea{flex:1;resize:none;min-height:44px;max-height:110px;padding:10px 12px;font:16px/1.4 Inter,system-ui,sans-serif;
58 58 border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none}
59 59 .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px;
60 60 background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer}
@@ -98,7 +98,7 @@
98 98 panel.setAttribute("aria-label", "KA Agent");
99 99 panel.innerHTML =
100 100 '<div class="kaa-head"><b>KA<span class="kaa-ka">Agent</span></b><small>Groupe KA · IA</small><span class="kaa-sp"></span>' +
101 − '<button class="kaa-hbtn kaa-x1" title="Réduire / plein écran" aria-label="Réduire ou remettre en plein écran">⛶</button>' +
101 + '<button class="kaa-hbtn kaa-x1" title="Plein écran" aria-label="Plein écran">⛶</button>' +
102 102 '<button class="kaa-hbtn kaa-x2" title="Fermer" aria-label="Fermer">✕</button></div>' +
103 103 '<div class="kaa-log"></div>' +
104 104 '<div class="kaa-in"><textarea rows="1" placeholder="Posez votre question…" aria-label="Votre question"></textarea>' +
@@ -143,20 +143,32 @@
143 143 }
144 144
145 145 /* ---------- ouverture / plein écran ---------- */
146 + /* Sur téléphone : panneau plein écran d'office + scroll d'arrière-plan
147 + verrouillé tant qu'il est ouvert (html.kaa-lock). Pas de focus clavier
148 + forcé au tap (le clavier ne s'ouvre que si l'utilisateur touche le champ). */
149 + var mobileMq = window.matchMedia("(max-width: 640px)");
150 + var fineInput = window.matchMedia("(hover: hover) and (pointer: fine)");
151 + function syncLock() {
152 + var open = panel.classList.contains("kaa-open");
153 + if (open && mobileMq.matches) panel.classList.add("kaa-full");
154 + document.documentElement.classList.toggle(
155 + "kaa-lock", open && panel.classList.contains("kaa-full"));
156 + }
146 157 btn.addEventListener("click", function () {
147 158 panel.classList.toggle("kaa-open");
148 159 if (panel.classList.contains("kaa-open")) {
149 − panel.classList.add("kaa-full"); /* toujours plein écran à l'ouverture */
150 − render(); ta.focus();
151 − } else {
152 − panel.classList.remove("kaa-full");
160 + render();
161 + if (fineInput.matches) ta.focus();
153 162 }
163 + syncLock();
154 164 });
155 165 panel.querySelector(".kaa-x2").addEventListener("click", function () {
156 166 panel.classList.remove("kaa-open", "kaa-full");
167 + syncLock();
157 168 });
158 169 panel.querySelector(".kaa-x1").addEventListener("click", function () {
159 170 panel.classList.toggle("kaa-full");
171 + syncLock();
160 172 scroll();
161 173 });
162 174
modified frontend/src/App.tsx +15 −1
@@ -4,6 +4,7 @@
4 4 // App.tsx : layout global (header + ticker en direct + footer encre) et routage
5 5 // -----------------------------------------------------------------------------
6 6 import { useEffect, useState } from "react";
7 +import { createPortal } from "react-dom";
7 8 import { NavLink, Route, Routes, useLocation } from "react-router-dom";
8 9 import {
9 10 fetchAuthConfig, fetchFacets, fetchSources, fetchStats,
@@ -93,6 +94,14 @@ function AccountMenu() {
93 94 fetchAuthConfig().then((c) => setEnabled(c.ka || c.google)).catch(() => {});
94 95 }, []);
95 96
97 + // menu ouvert : fermeture à Escape (le tap extérieur passe par le backdrop)
98 + useEffect(() => {
99 + if (!menuOpen) return;
100 + const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setMenuOpen(false); };
101 + window.addEventListener("keydown", onKey);
102 + return () => window.removeEventListener("keydown", onKey);
103 + }, [menuOpen]);
104 +
96 105 if (!enabled) return null;
97 106 if (!me) {
98 107 return (
@@ -132,7 +141,12 @@ function AccountMenu() {
132 141 </button>
133 142 {menuOpen && (
134 143 <>
135 − <div className="account-backdrop" onClick={() => setMenuOpen(false)} aria-hidden="true" />
144 + {/* Backdrop rendu à la racine (createPortal) : le backdrop-filter du
145 + header re-parentait le position:fixed → voile limité au header. */}
146 + {createPortal(
147 + <div className="account-backdrop" onClick={() => setMenuOpen(false)} aria-hidden="true" />,
148 + document.body,
149 + )}
136 150 <div className="account-menu" role="menu">
137 151 <div className="account-id">
138 152 <b>{me.name || "Mon compte"}</b>
modified frontend/src/ka/tokens.css +87 −0
@@ -69,7 +69,24 @@
69 69
70 70 /* Cible tactile minimale */
71 71 --touch: 44px;
72 +
73 + /* ---- Échelle z-index COMMUNE (obligatoire — aucune valeur arbitraire) ----
74 + grain de film body::before = 9999 (pointer-events:none, toujours au-dessus,
75 + purement visuel). Tout composant interactif se place sous 9999 : */
76 + --z-content: 1; /* contenu positionné ordinaire */
77 + --z-sticky: 300; /* éléments sticky de contenu (sous-nav…) */
78 + --z-header: 500; /* header du site */
79 + --z-bottombar: 600; /* tab bar / barre d'action basse */
80 + --z-dropdown: 700; /* menus déroulants (au-dessus des barres) */
81 + --z-overlay: 800; /* voile sous modale/panneau */
82 + --z-modal: 900; /* modales, panneaux de filtres, galeries */
83 + --z-toast: 950; /* notifications */
84 +
85 + /* ---- Hauteur de viewport DYNAMIQUE (barre du navigateur mobile qui se
86 + replie au scroll) : toujours var(--vh100), JAMAIS 100vh en dur. ---- */
87 + --vh100: 100vh;
72 88 }
89 +@supports (height: 100dvh) { :root { --vh100: 100dvh; } }
73 90
74 91 /* ---- Socle ---- */
75 92 * { box-sizing: border-box; }
@@ -227,3 +244,73 @@ body > * { position: relative; }
227 244 /* ---- Utilitaires responsive ---- */
228 245 .only-mobile { display: initial; } .only-desktop { display: none; }
229 246 @media (min-width: 768px) { .only-mobile { display: none; } .only-desktop { display: initial; } }
247 +
248 +/* =============================================================================
249 + SOCLE MOBILE OBLIGATOIRE (2026-08-19) — règles communes aux 13 sites.
250 + RÈGLES pour tout composant futur :
251 + · TAP, jamais :hover, pour ouvrir un menu (le survol n'existe pas au doigt) ;
252 + le :hover ne sert qu'aux effets décoratifs, sous @media (hover: hover).
253 + · var(--vh100) (dvh) et JAMAIS 100vh pour tout élément calé sur le viewport.
254 + · position:fixed exige qu'AUCUN ancêtre n'ait transform/filter/perspective/
255 + will-change/backdrop-filter — sinon monter l'élément à la racine (body).
256 + · Barre basse fixe = .ka-bottombar + classe has-bottombar sur <body>.
257 + · Menu/panneau ouvert = .ka-scroll-lock sur <html> (scroll arrière-plan gelé).
258 + · z-index : uniquement l'échelle --z-* ci-dessus.
259 + · Zones tactiles ≥ var(--touch) (44 px), champs ≥ 16 px (zoom iOS).
260 + ========================================================================== */
261 +
262 +/* Anti-zoom iOS : au doigt, aucun champ sous 16 px (le focus d'un champ <16 px
263 + déclenche un zoom automatique de page sur Safari iOS). !important assumé :
264 + c'est un filet d'accessibilité, un champ plus petit casse le zoom quoi
265 + qu'il arrive — ne s'applique qu'aux écrans tactiles. */
266 +@media (hover: none) and (pointer: coarse) {
267 + input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
268 + select, textarea { font-size: max(16px, 1em) !important; }
269 +}
270 +
271 +/* Zone tactile du badge Groupe KA étendue à ≥44 px au doigt, sans changer
272 + son rendu (débord de la zone cliquable via pseudo-élément). */
273 +@media (pointer: coarse) {
274 + .gk-badge { position: relative; }
275 + .gk-badge::after { content: ""; position: absolute; inset: -8px; }
276 +}
277 +
278 +/* Hauteurs viewport dynamiques */
279 +.h-viewport { height: 100vh; height: 100dvh; }
280 +.min-h-viewport { min-height: 100vh; min-height: 100dvh; }
281 +
282 +/* Barre fixe basse fiable : safe-area iPhone incluse, immunisée contre les
283 + transforms accidentels, et compensation d'espace via body.has-bottombar. */
284 +.ka-bottombar {
285 + position: fixed; left: 0; right: 0; bottom: 0;
286 + z-index: var(--z-bottombar);
287 + padding-bottom: env(safe-area-inset-bottom, 0px);
288 + background: var(--surface);
289 + border-top: 1.5px solid var(--ink);
290 + transform: none !important; filter: none !important;
291 +}
292 +body.has-bottombar { padding-bottom: calc(var(--bottombar-h, 64px) + env(safe-area-inset-bottom, 0px)); }
293 +
294 +/* Verrou de scroll d'arrière-plan (menu mobile, modale, panneau de filtres
295 + ouverts) : ajouter/retirer .ka-scroll-lock sur <html>. */
296 +html.ka-scroll-lock, html.ka-scroll-lock body { overflow: hidden !important; overscroll-behavior: none; }
297 +
298 +/* Menu déroulant de référence : au-dessus de tout contenu (cartes Mapbox
299 + incluses), défilement interne avec élan, jamais plus haut que l'écran. */
300 +.ka-menu {
301 + position: absolute; z-index: var(--z-dropdown);
302 + min-width: 180px;
303 + max-height: min(60vh, 420px); max-height: min(60dvh, 420px);
304 + overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
305 + background: var(--surface); border: 1.5px solid var(--ink);
306 + border-radius: var(--r-card); box-shadow: var(--shadow-off-mid);
307 +}
308 +.ka-menu a, .ka-menu button, .ka-menu label, .ka-menu [role="menuitem"], .ka-menu [role="option"] {
309 + display: flex; align-items: center; min-height: var(--touch);
310 + padding: 10px 14px; width: 100%; text-decoration: none;
311 +}
312 +/* Voile plein écran sous une modale / un panneau */
313 +.ka-overlay {
314 + position: fixed; inset: 0; z-index: var(--z-overlay);
315 + background: rgba(20, 24, 20, 0.45);
316 +}
modified frontend/src/pages/Home.tsx +12 −0
@@ -73,6 +73,18 @@ export default function Home() {
73 73 // feuille de filtres mobile (bottom sheet) + panneau avancé desktop
74 74 const [sheetOpen, setSheetOpen] = useState(false);
75 75 const [advOpen, setAdvOpen] = useState(false);
76 + // feuille ouverte : gel du scroll d'arrière-plan + fermeture à Escape
77 + // (même pattern que le menu hamburger d'App.tsx)
78 + useEffect(() => {
79 + document.body.style.overflow = sheetOpen ? "hidden" : "";
80 + if (!sheetOpen) return () => { document.body.style.overflow = ""; };
81 + const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setSheetOpen(false); };
82 + window.addEventListener("keydown", onKey);
83 + return () => {
84 + document.body.style.overflow = "";
85 + window.removeEventListener("keydown", onKey);
86 + };
87 + }, [sheetOpen]);
76 88 const activeFilters = [q, city, sector, source, priceMin, priceMax, unitType,
77 89 dispo, pets, furnished, areaMin, deal, kaMin].filter(Boolean).length;
78 90 const advCount = [dispo, pets, furnished, areaMin, source, kaMin].filter(Boolean).length;
modified frontend/src/styles.css +15 −3
@@ -643,9 +643,17 @@ html, body { overflow-x: clip; }
643 643 .footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }
644 644 }
645 645
646 −/* Anti-zoom iOS : les champs doivent faire >= 16px */
646 +/* Anti-zoom iOS : les champs doivent faire >= 16px
647 + (filet global : tokens.css force aussi 16 px au doigt) */
647 648 @media (max-width: 900px) {
648 − .field input, .field select { font-size: 16px; }
649 + .field input, .field select,
650 + .f-search input, .f-native, .f-ctl select, .ms-sort select { font-size: 16px; }
651 +}
652 +
653 +/* Cible tactile ≥44 px du logo (124×30 rendus) : zone étendue invisible */
654 +@media (pointer: coarse) {
655 + .brand { position: relative; }
656 + .brand::after { content: ""; position: absolute; inset: -8px; }
649 657 }
650 658
651 659 /* ================= Footer ================= */
@@ -1193,7 +1201,11 @@ html { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */
1193 1201 }
1194 1202 .account-btn img { width: 100%; height: 100%; object-fit: cover; }
1195 1203 .account-initial { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--orange); }
1196 −.account-backdrop { position: fixed; inset: 0; z-index: 90; }
1204 +/* Click-catcher plein écran rendu via createPortal à la racine de <body>
1205 + (un fixed descendant du header blurré se re-parentait → 66 px de haut).
1206 + z-index SOUS le header (50) : le menu .account-menu reste ancré DANS le
1207 + header et doit peindre au-dessus du voile. */
1208 +.account-backdrop { position: fixed; inset: 0; z-index: 40; }
1197 1209 .account-menu {
1198 1210 position: absolute; right: 0; top: calc(100% + 10px); z-index: 91;
1199 1211 min-width: 220px; background: var(--surface); border: 1.5px solid var(--ink);
1200 1212