SPB Git

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

Correctifs front-end mobile : viewport-fit=cover, safe-area et verrou de scroll cookies, socle mobile ka-ui

- layout.tsx : export viewport (device-width, initial-scale 1, viewport-fit=cover) pour les encoches iOS ; extension tactile ::after -8px sur le logo Vrai-Prix du header (cible >= 44 px)
- CookieConsent.tsx : padding bas du bandeau avec env(safe-area-inset-bottom) ; verrou du scroll d arriere-plan quand le panneau de preferences est ouvert (useEffect + cleanup) + fermeture a Escape
- src/ka/tokens.css : nouvelle section SOCLE MOBILE OBLIGATOIRE (z-index unifies, --vh100 dvh, .ka-bottombar, verrou ka-scroll-lock, filet anti-zoom iOS 16 px, extension tactile gk-badge)
- public/ka-agent.js : agent corrige (safe-area bulle/panneau, textarea 16 px, plein ecran auto <= 640 px + verrou scroll kaa-lock)

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

4 changed files +137 −10

modified public/ka-agent.js +25 −7
@@ -17,18 +17,19 @@
17 17
18 /* ---------- styles ---------- */ 18 /* ---------- styles ---------- */
19 var css = ` 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 border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b); 21 border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b);
22 color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif; 22 color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif;
23 cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center; 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 .kaa-btn:hover{transform:translate(-2px,-2px)} 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)); 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(100dvh - 110px));display:none;flex-direction:column;background:var(--paper,#f5f3ee); 27+ height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px));display:none;flex-direction:column;background:var(--paper,#f5f3ee);
28 border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85); 28 border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85);
29 overflow:hidden;font-family:Inter,system-ui,sans-serif} 29 overflow:hidden;font-family:Inter,system-ui,sans-serif}
30 .kaa-panel.kaa-open{display:flex} 30 .kaa-panel.kaa-open{display:flex}
31− .kaa-panel.kaa-full{right:0;bottom:0;width:100vw;height:100dvh;border-radius:0;border:0;box-shadow:none} 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}
32 .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814); 33 .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814);
33 color:var(--paper,#f5f3ee);flex:none} 34 color:var(--paper,#f5f3ee);flex:none}
34 .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em} 35 .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em}
@@ -53,7 +54,7 @@
53 border-radius:999px;background:var(--surface-2,#faf9f5)} 54 border-radius:999px;background:var(--surface-2,#faf9f5)}
54 .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none; 55 .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none;
55 padding-bottom:calc(11px + env(safe-area-inset-bottom,0))} 56 padding-bottom:calc(11px + env(safe-area-inset-bottom,0))}
56− .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;
57 border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none} 58 border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none}
58 .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px; 59 .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px;
59 background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer} 60 background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer}
@@ -142,15 +143,32 @@
142 } 143 }
143 144
144 /* ---------- ouverture / plein écran ---------- */ 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+ }
145 btn.addEventListener("click", function () { 157 btn.addEventListener("click", function () {
146 panel.classList.toggle("kaa-open"); 158 panel.classList.toggle("kaa-open");
147− if (panel.classList.contains("kaa-open")) { render(); ta.focus(); } 159+ if (panel.classList.contains("kaa-open")) {
160+ render();
161+ if (fineInput.matches) ta.focus();
162+ }
163+ syncLock();
148 }); 164 });
149 panel.querySelector(".kaa-x2").addEventListener("click", function () { 165 panel.querySelector(".kaa-x2").addEventListener("click", function () {
150 panel.classList.remove("kaa-open", "kaa-full"); 166 panel.classList.remove("kaa-open", "kaa-full");
167+ syncLock();
151 }); 168 });
152 panel.querySelector(".kaa-x1").addEventListener("click", function () { 169 panel.querySelector(".kaa-x1").addEventListener("click", function () {
153 panel.classList.toggle("kaa-full"); 170 panel.classList.toggle("kaa-full");
171+ syncLock();
154 scroll(); 172 scroll();
155 }); 173 });
156 174
modified src/app/layout.tsx +9 −2
@@ -1,5 +1,5 @@
1 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai 1 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2−import type { Metadata } from "next"; 2+import type { Metadata, Viewport } from "next";
3 import { Inter, JetBrains_Mono, Space_Grotesk } from "next/font/google"; 3 import { Inter, JetBrains_Mono, Space_Grotesk } from "next/font/google";
4 import "./globals.css"; 4 import "./globals.css";
5 import { LangProvider, LangToggle } from "@/components/LangContext"; 5 import { LangProvider, LangToggle } from "@/components/LangContext";
@@ -56,6 +56,13 @@ export const metadata: Metadata = {
56 formatDetection: { telephone: false }, 56 formatDetection: { telephone: false },
57 }; 57 };
58 58
59+/** Viewport mobile : viewport-fit=cover pour les encoches iOS (safe-area). */
60+export const viewport: Viewport = {
61+ width: "device-width",
62+ initialScale: 1,
63+ viewportFit: "cover",
64+};
65+
59 /** JSON-LD global : l'organisation et le site (entités schema.org). */ 66 /** JSON-LD global : l'organisation et le site (entités schema.org). */
60 const ORG_JSONLD = { 67 const ORG_JSONLD = {
61 "@context": "https://schema.org", 68 "@context": "https://schema.org",
@@ -109,7 +116,7 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
109 <div className="relative mx-auto flex h-16 max-w-6xl items-center gap-5 px-4 sm:px-6"> 116 <div className="relative mx-auto flex h-16 max-w-6xl items-center gap-5 px-4 sm:px-6">
110 <Link 117 <Link
111 href="/" 118 href="/"
112− className="vp-display group flex items-center text-[22px] font-bold leading-none tracking-[-0.04em] sm:text-[26px]" 119+ className="vp-display group relative flex items-center text-[22px] font-bold leading-none tracking-[-0.04em] after:absolute after:-inset-2 after:content-[''] sm:text-[26px]"
113 > 120 >
114 Vrai 121 Vrai
115 <span className="ml-1 rotate-[-2deg] rounded-md bg-ink px-[7px] pb-1 pt-[2px] text-lime transition-transform duration-200 group-hover:rotate-0"> 122 <span className="ml-1 rotate-[-2deg] rounded-md bg-ink px-[7px] pb-1 pt-[2px] text-lime transition-transform duration-200 group-hover:rotate-0">
modified src/components/CookieConsent.tsx +16 −1
@@ -129,6 +129,21 @@ export default function CookieConsent() {
129 }; 129 };
130 }, []); 130 }, []);
131 131
132+ // Panneau ouvert : verrou du scroll d'arrière-plan + fermeture à Escape (avec cleanup).
133+ useEffect(() => {
134+ if (!panel) return;
135+ const prev = document.body.style.overflow;
136+ document.body.style.overflow = "hidden";
137+ const onKey = (e: KeyboardEvent) => {
138+ if (e.key === "Escape") setPanel(false);
139+ };
140+ window.addEventListener("keydown", onKey);
141+ return () => {
142+ document.body.style.overflow = prev;
143+ window.removeEventListener("keydown", onKey);
144+ };
145+ }, [panel]);
146+
132 const persist = (preferences: boolean, analyticsOn: boolean) => { 147 const persist = (preferences: boolean, analyticsOn: boolean) => {
133 const c: ConsentState = { 148 const c: ConsentState = {
134 essential: true, 149 essential: true,
@@ -148,7 +163,7 @@ export default function CookieConsent() {
148 return ( 163 return (
149 <> 164 <>
150 {showBanner && ( 165 {showBanner && (
151− <div className="fixed inset-x-0 bottom-0 z-[1300] p-3 sm:p-5"> 166+ <div className="fixed inset-x-0 bottom-0 z-[1300] p-3 pb-[calc(12px+env(safe-area-inset-bottom))] sm:p-5 sm:pb-[calc(20px+env(safe-area-inset-bottom))]">
152 <div className="mx-auto max-w-3xl rounded-[10px] border-[1.5px] border-ink bg-ink p-5 text-paper shadow-[0_-8px_32px_rgba(16,18,16,0.35),6px_6px_0_rgba(20,24,20,0.3)]"> 167 <div className="mx-auto max-w-3xl rounded-[10px] border-[1.5px] border-ink bg-ink p-5 text-paper shadow-[0_-8px_32px_rgba(16,18,16,0.35),6px_6px_0_rgba(20,24,20,0.3)]">
153 <p className="klabel !text-lime">🍪 {t.prefsTitle}</p> 168 <p className="klabel !text-lime">🍪 {t.prefsTitle}</p>
154 <p className="mt-2 text-[13.5px] leading-relaxed text-[rgba(245,243,238,0.85)]"> 169 <p className="mt-2 text-[13.5px] leading-relaxed text-[rgba(245,243,238,0.85)]">
modified src/ka/tokens.css +87 −0
@@ -69,7 +69,24 @@
69 69
70 /* Cible tactile minimale */ 70 /* Cible tactile minimale */
71 --touch: 44px; 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 /* ---- Socle ---- */ 91 /* ---- Socle ---- */
75 * { box-sizing: border-box; } 92 * { box-sizing: border-box; }
@@ -227,3 +244,73 @@ body > * { position: relative; }
227 /* ---- Utilitaires responsive ---- */ 244 /* ---- Utilitaires responsive ---- */
228 .only-mobile { display: initial; } .only-desktop { display: none; } 245 .only-mobile { display: initial; } .only-desktop { display: none; }
229 @media (min-width: 768px) { .only-mobile { display: none; } .only-desktop { display: initial; } } 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+}
230 317
Menu

Theme