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>
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 | ||||