SPB Git forge

spb/groupe-ka

Public

Groupe KA — site du holding + KA ID (compte unique & SSO des 7 plateformes). Next.js 16, SQLite, Google & Apple login.

81commits 1branches 0releases
89.2 MBsize
maindefault branch
22 days agolast push
TypeScript 70.4% HTML 18.4% JavaScript 4% Python 3.8% CSS 3.4%

Mobile : overlay du menu sorti du header (fix backdrop-filter/fixed), import tokens.css layer(base), verrou scroll, viewport-fit=cover, stat-chips tactiles, ka-agent corrigé

- src/app/layout.tsx : overlay #gk-menu déplacé hors du <header> (le backdrop-blur
  re-parentait le position:fixed → menu inutilisable entre 768 et 1280 px) ;
  export viewport (device-width, initial-scale 1, viewport-fit cover) ;
  header sur var(--z-header), overlay sur var(--z-modal), nav scrollable.
- src/app/globals.css : @import "../ka/tokens.css" layer(base) (socle mobile
  commun : échelle z-index, anti-zoom iOS 16 px, overflow-x clip → fin du
  débordement horizontal du ticker) ; grain dupliqué retiré (un seul grain) ;
  pilotage CSS pur du menu (body:has(#gk-menu:checked)) + gel du scroll ;
  zone tactile ≥44 px des a.stat-chip (::after inset -6px).
- src/ka/tokens.css : socle mobile obligatoire (z-index communs, --vh100,
  .ka-bottombar, .ka-scroll-lock, .ka-menu/.ka-overlay, filet anti-zoom iOS).
- public/ka-agent.js : safe-areas, textarea 16 px, plein écran ≤640 px + verrou.

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

4 changed files +209 −58

modified public/ka-agent.js +25 −7
@@ -17,18 +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;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 33 .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814);
33 34 color:var(--paper,#f5f3ee);flex:none}
34 35 .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em}
@@ -53,7 +54,7 @@
53 54 border-radius:999px;background:var(--surface-2,#faf9f5)}
54 55 .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none;
55 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 58 border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none}
58 59 .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px;
59 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 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 157 btn.addEventListener("click", function () {
146 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 165 panel.querySelector(".kaa-x2").addEventListener("click", function () {
150 166 panel.classList.remove("kaa-open", "kaa-full");
167 + syncLock();
151 168 });
152 169 panel.querySelector(".kaa-x1").addEventListener("click", function () {
153 170 panel.classList.toggle("kaa-full");
171 + syncLock();
154 172 scroll();
155 173 });
156 174
modified src/app/globals.css +44 −10
@@ -9,6 +9,10 @@
9 9 */
10 10
11 11 @import "tailwindcss";
12 +/* Socle commun Groupe KA (tokens + socle mobile). ⚠️ layer(base) OBLIGATOIRE :
13 + sans lui, l'import écrase les utilities Tailwind v4 (modèle : vrai-prix).
14 + Les redéfinitions locales ci-dessous restent hors layer et gagnent. */
15 +@import "../ka/tokens.css" layer(base);
12 16
13 17 :root {
14 18 --paper: #f5f3ee;
@@ -67,19 +71,36 @@ body {
67 71 -webkit-font-smoothing: antialiased;
68 72 }
69 73
70 −/* Grain de film pleine page — jamais de z-index sur body > * (position: relative
71 − seulement) pour ne pas casser les utilitaires z-* de Tailwind. */
72 −body::before {
73 − content: "";
74 +/* Grain de film pleine page : fourni par ../ka/tokens.css (body::before) —
75 + la copie locale a été retirée pour éviter le double grain (plus sombre).
76 + Jamais de z-index sur body > * (position: relative seulement, via tokens). */
77 +
78 +/* ---------- Menu mobile plein écran (overlay HORS du header) ----------
79 + L'overlay est un enfant direct de <body> : un position:fixed descendant du
80 + header (backdrop-blur) se re-parentait et ne couvrait que le header.
81 + Piloté 100 % CSS par la checkbox #gk-menu restée dans le header. */
82 +.gk-mobile-overlay {
83 + display: none;
74 84 position: fixed;
75 85 inset: 0;
76 − pointer-events: none;
77 − opacity: 0.35;
78 − z-index: 9999;
79 − background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.06'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
86 + z-index: var(--z-modal);
80 87 }
81 −body > * {
82 − position: relative;
88 +body:has(#gk-menu:checked) .gk-mobile-overlay {
89 + display: flex;
90 +}
91 +/* Menu ouvert : gel du scroll d'arrière-plan */
92 +html:has(#gk-menu:checked) {
93 + overflow: hidden;
94 +}
95 +/* Desktop (nav complète visible) : jamais d'overlay, même si la case est
96 + restée cochée après un redimensionnement. */
97 +@media (min-width: 1280px) {
98 + .gk-mobile-overlay {
99 + display: none !important;
100 + }
101 + html:has(#gk-menu:checked) {
102 + overflow: auto;
103 + }
83 104 }
84 105
85 106 h1,
@@ -678,6 +699,19 @@ h4 {
678 699 box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.12);
679 700 }
680 701
702 +/* Cible tactile ≥44 px des pilules cliquables (33 px rendus + 2×6 px de zone
703 + étendue) — sans changer le rendu. */
704 +@media (pointer: coarse) {
705 + a.stat-chip {
706 + position: relative;
707 + }
708 + a.stat-chip::after {
709 + content: "";
710 + position: absolute;
711 + inset: -6px;
712 + }
713 +}
714 +
681 715 .pulse-dot {
682 716 position: relative;
683 717 width: 8px;
modified src/app/layout.tsx +53 −41
@@ -1,5 +1,5 @@
1 1 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 −import type { Metadata } from "next";
2 +import type { Metadata, Viewport } from "next";
3 3 import { Inter, JetBrains_Mono, Space_Grotesk } from "next/font/google";
4 4 import UserButton from "./UserButton";
5 5 import { getLiveMetrics, fmtDec, fmtInt, fmtPlus, type LiveMetrics } from "@/lib/live";
@@ -42,6 +42,13 @@ export const metadata: Metadata = {
42 42 },
43 43 };
44 44
45 +// viewport-fit=cover : nécessaire pour les safe-areas iOS (encoche/barre home).
46 +export const viewport: Viewport = {
47 + width: "device-width",
48 + initialScale: 1,
49 + viewportFit: "cover",
50 +};
51 +
45 52 const NAV_ITEMS = [
46 53 { href: "/#trouve-ka", label: "Moteur" },
47 54 { href: "/#flux", label: "L’agrégation" },
@@ -110,7 +117,7 @@ export default async function RootLayout({
110 117 <body
111 118 className={`${spaceGrotesk.variable} ${inter.variable} ${jetbrains.variable} bg-paper text-ink`}
112 119 >
113 − <header className="sticky top-0 z-[1100] border-b-2 border-ink bg-paper md:bg-[rgba(245,243,238,0.88)] md:backdrop-blur-xl">
120 + <header className="sticky top-0 z-[var(--z-header)] border-b-2 border-ink bg-paper md:bg-[rgba(245,243,238,0.88)] md:backdrop-blur-xl">
114 121 <div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6">
115 122 <a href="#" className="group" aria-label="Groupe KA — accueil">
116 123 <Wordmark />
@@ -155,49 +162,54 @@ export default async function RootLayout({
155 162 >
156 163 ☰
157 164 </label>
158 − <div className="fixed inset-0 z-[3000] hidden flex-col bg-paper peer-checked:flex">
159 − <div className="flex h-16 items-center justify-between border-b-2 border-ink px-4">
160 − <Wordmark />
161 − <label
162 − htmlFor="gk-menu"
163 − className="flex h-11 w-11 cursor-pointer items-center justify-center rounded-md border-[1.5px] border-ink text-[18px] shadow-[3px_3px_0_rgba(20,24,20,0.25)] select-none"
164 − >
165 − ✕
166 − </label>
167 − </div>
168 − <nav aria-label="Menu mobile" className="px-6 pt-8">
169 − <ul>
170 − {NAV_ITEMS.map((item, i) => (
171 − <li
172 − key={item.href}
173 − className="border-b border-dashed border-[rgba(20,24,20,0.25)]"
174 − >
175 − <a
176 − href={item.href}
177 − className="gk-display flex items-baseline gap-4 py-5 text-[29px] font-bold tracking-[-0.03em] uppercase"
178 − >
179 − <span className="gk-mono text-[12px] font-bold text-green">
180 − 0{i + 1}
181 − </span>
182 − {item.label}
183 − </a>
184 − </li>
185 − ))}
186 − <li className="pt-6">
187 − <a
188 − href="mailto:contact@groupe-ka.com"
189 − className="btn btn-primary w-full"
190 − >
191 − contact@groupe-ka.com
192 − </a>
193 − </li>
194 − </ul>
195 − </nav>
196 − </div>
197 165 </div>
198 166 </div>
199 167 </header>
200 168
169 + {/* Overlay du menu mobile — HORS du header (le backdrop-blur du header
170 + re-parentait le position:fixed → menu inutilisable 768-1280 px).
171 + Piloté en CSS pur : body:has(#gk-menu:checked) .gk-mobile-overlay
172 + (globals.css) — les <label htmlFor="gk-menu"> marchent à distance. */}
173 + <div className="gk-mobile-overlay flex-col bg-paper">
174 + <div className="flex h-16 items-center justify-between border-b-2 border-ink px-4">
175 + <Wordmark />
176 + <label
177 + htmlFor="gk-menu"
178 + className="flex h-11 w-11 cursor-pointer items-center justify-center rounded-md border-[1.5px] border-ink text-[18px] shadow-[3px_3px_0_rgba(20,24,20,0.25)] select-none"
179 + >
180 + ✕
181 + </label>
182 + </div>
183 + <nav aria-label="Menu mobile" className="min-h-0 flex-1 overflow-y-auto px-6 pt-8 pb-10">
184 + <ul>
185 + {NAV_ITEMS.map((item, i) => (
186 + <li
187 + key={item.href}
188 + className="border-b border-dashed border-[rgba(20,24,20,0.25)]"
189 + >
190 + <a
191 + href={item.href}
192 + className="gk-display flex items-baseline gap-4 py-5 text-[29px] font-bold tracking-[-0.03em] uppercase"
193 + >
194 + <span className="gk-mono text-[12px] font-bold text-green">
195 + 0{i + 1}
196 + </span>
197 + {item.label}
198 + </a>
199 + </li>
200 + ))}
201 + <li className="pt-6">
202 + <a
203 + href="mailto:contact@groupe-ka.com"
204 + className="btn btn-primary w-full"
205 + >
206 + contact@groupe-ka.com
207 + </a>
208 + </li>
209 + </ul>
210 + </nav>
211 + </div>
212 +
201 213 <div className="ticker" aria-hidden="true">
202 214 <div className="ticker-inner">
203 215 {[0, 1].map((dup) => (
modified 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 +}
230 317