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%

tokens.css : overflow-x clip aussi sur html (quirk WebKit iOS) ; sync.sh : chemin trouve-ka corrigé (apps/web/ka) ; audits : mesure du défilement latéral réel (clip), zone tactile effective (::after), ciblage du vrai bouton menu, compensation via wrapper/#root et bandeaux témoins exclus

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

4 changed files +60 −20

modified mobile-audit.mjs +45 −16
@@ -38,11 +38,19 @@ const PAGE_AUDIT = `(() => {
38 38 const mv = document.querySelector('meta[name="viewport"]');
39 39 res.viewport = mv ? mv.getAttribute("content") : null;
40 40
41 − // débordement horizontal + fautifs
41 + // débordement horizontal RÉEL (scrollWidth peut mentir quand overflow-x:clip
42 + // est posé — on vérifie que le viewport défile vraiment)
42 43 const iw = window.innerWidth;
43 − res.overflowPx = Math.max(
44 + const rawOverflow = Math.max(
44 45 document.documentElement.scrollWidth - iw,
45 46 document.body ? document.body.scrollWidth - iw : 0);
47 + res.overflowPx = 0;
48 + if (rawOverflow > 1) {
49 + const x0 = window.scrollX;
50 + window.scrollBy(rawOverflow, 0);
51 + if (window.scrollX - x0 > 1) res.overflowPx = rawOverflow;
52 + window.scrollTo(x0, 0);
53 + }
46 54 res.overflowEls = [];
47 55 if (res.overflowPx > 1) {
48 56 for (const el of document.querySelectorAll("body *")) {
@@ -105,7 +113,11 @@ const PAGE_AUDIT = `(() => {
105 113 const bodyPad = parseFloat(getComputedStyle(document.body).paddingBottom) || 0;
106 114 const main = document.querySelector("main");
107 115 const mainPad = main ? parseFloat(getComputedStyle(main).paddingBottom) || 0 : 0;
108 − res.bottomBar = { sel: bar.sel, h: bar.rect.h, bodyPadBottom: bodyPad, mainPadBottom: mainPad };
116 + let wrapPad = 0;
117 + for (const w of document.querySelectorAll("#root, #__next, .app, #app")) {
118 + wrapPad = Math.max(wrapPad, parseFloat(getComputedStyle(w).paddingBottom) || 0);
119 + }
120 + res.bottomBar = { sel: bar.sel, h: bar.rect.h, bodyPadBottom: bodyPad, mainPadBottom: mainPad, wrapPadBottom: wrapPad };
109 121 }
110 122
111 123 // champs zoom iOS
@@ -120,13 +132,28 @@ const PAGE_AUDIT = `(() => {
120 132 if (res.smallInputs.length >= 10) break;
121 133 }
122 134
123 − // cibles tactiles dans header/nav
135 + // cibles tactiles dans header/nav — en tenant compte des extensions de zone
136 + // par pseudo-élément (::after/::before absolu avec inset négatif)
137 + const hitExt = (el) => {
138 + let ext = 0;
139 + for (const ps of ["::after", "::before"]) {
140 + const cs = getComputedStyle(el, ps);
141 + if (cs.content !== "none" && cs.position === "absolute") {
142 + const t = parseFloat(cs.top), b = parseFloat(cs.bottom);
143 + if (!isNaN(t) && t < 0) ext = Math.max(ext, -t - (isNaN(b) ? 0 : Math.min(b, 0)));
144 + else if (!isNaN(t) && !isNaN(b)) ext = Math.max(ext, Math.max(0, -t) + Math.max(0, -b));
145 + }
146 + }
147 + return ext;
148 + };
124 149 res.smallTargets = [];
125 150 for (const el of document.querySelectorAll("header a, header button, nav a, nav button, [class*=menu] a, [class*=menu] button")) {
126 151 const r = el.getBoundingClientRect();
127 152 if (r.width === 0 || r.height === 0) continue;
128 − if ((r.height < 34 || r.width < 34) && r.top >= 0 && r.top < window.innerHeight) {
129 − res.smallTargets.push({ sel: sel(el), w: Math.round(r.width), h: Math.round(r.height), text: (el.textContent || "").trim().slice(0, 20) });
153 + const ext = hitExt(el);
154 + const effH = r.height + 2 * ext, effW = r.width + 2 * ext;
155 + if ((effH < 34 || effW < 34) && r.top >= 0 && r.top < window.innerHeight) {
156 + res.smallTargets.push({ sel: sel(el), w: Math.round(effW), h: Math.round(effH), text: (el.textContent || "").trim().slice(0, 20) });
130 157 if (res.smallTargets.length >= 10) break;
131 158 }
132 159 }
@@ -230,8 +257,8 @@ async function auditPage(page, url, tag, shot) {
230 257 if (d.overflowPx > 1) r.issues.push(`débordement horizontal +${d.overflowPx}px : ${(d.overflowEls || []).map((e) => e.sel).slice(0, 4).join(", ")}`);
231 258 for (const f of d.fixed || []) if (f.brokenBy) r.issues.push(`fixed cassé par ancêtre ${f.brokenBy} → [${f.sel}]`);
232 259 const bb = d.bottomBar;
233 − if (bb && bb.bodyPadBottom < bb.h - 8 && bb.mainPadBottom < bb.h - 8)
234 − r.issues.push(`pas de compensation padding sous la barre basse [${bb.sel}] h=${bb.h}px (body ${bb.bodyPadBottom}px / main ${bb.mainPadBottom}px)`);
260 + if (bb && !/cookie|consent|banner/i.test(bb.sel) && bb.bodyPadBottom < bb.h - 8 && bb.mainPadBottom < bb.h - 8 && (bb.wrapPadBottom ?? 0) < bb.h - 8)
261 + r.issues.push(`pas de compensation padding sous la barre basse [${bb.sel}] h=${bb.h}px (body ${bb.bodyPadBottom}px / main ${bb.mainPadBottom}px / wrapper ${bb.wrapPadBottom ?? 0}px)`);
235 262 if (bar && !/env\(|calc\(/.test(bar.cssBottom || "") && r.css.safeArea === 0)
236 263 r.issues.push(`aucune trace de safe-area-inset-bottom (barre basse [${bar.sel}])`);
237 264 if (r.css.vh100 > 0 && r.css.dvh === 0 && r.css.svh === 0) r.issues.push(`${r.css.vh100}× 100vh sans dvh/svh`);
@@ -239,22 +266,24 @@ async function auditPage(page, url, tag, shot) {
239 266 if (d.smallInputs.length) r.issues.push(`${d.smallInputs.length} champ(s) <16px (zoom iOS) : ${d.smallInputs.slice(0, 3).map((i) => i.sel).join(", ")}`);
240 267 if (d.smallTargets.length) r.issues.push(`${d.smallTargets.length} cible(s) tactile(s) <34px : ${d.smallTargets.slice(0, 3).map((t) => t.sel + "(" + t.w + "×" + t.h + ")").join(", ")}`);
241 268
242 − // test tap : ouvrir le premier bouton menu/hamburger trouvé
269 + // test tap : ouvrir le VRAI bouton de menu (aria-label/menu-btn/burger en
270 + // priorité — pas n'importe quel bouton du header), vérifier qu'un panneau
271 + // s'ouvre réellement (aria-expanded / classe open / dialog) puis le verrou.
243 272 try {
244 − const btn = page.locator('header button, [class*="burger"], [class*="hamburger"], button[aria-label*="menu" i], button[aria-label*="Menu"]').first();
273 + const btn = page.locator('button[aria-label*="menu" i], .menu-btn, [class*="burger"], [class*="hamburger"], label[for*="menu"]').first();
245 274 if (await btn.count()) {
246 − const before = await page.evaluate(() => document.body.innerHTML.length);
247 275 await btn.tap({ timeout: 3000 }).catch(() => btn.click({ timeout: 3000 }));
248 276 await page.waitForTimeout(600);
249 277 const after = await page.evaluate(() => ({
250 − len: document.body.innerHTML.length,
251 − bodyOverflow: getComputedStyle(document.body).overflow,
278 + opened: !!document.querySelector('.mobile-menu.open, [role="dialog"], .gk-mobile-overlay, [aria-expanded="true"], .mm-backdrop, input[id*="menu"]:checked'),
279 + bodyOverflow: (document.body.style.overflow || getComputedStyle(document.body).overflow),
252 280 htmlOverflow: getComputedStyle(document.documentElement).overflow,
281 + htmlLock: document.documentElement.classList.contains("ka-scroll-lock") || document.documentElement.classList.contains("kaa-lock"),
253 282 }));
254 − const opened = Math.abs(after.len - before) > 50;
255 − if (opened && after.bodyOverflow !== "hidden" && after.htmlOverflow !== "hidden")
283 + if (after.opened && !after.htmlLock &&
284 + !/hidden/.test(after.bodyOverflow) && !/hidden/.test(after.htmlOverflow))
256 285 r.issues.push("menu ouvert SANS blocage du scroll d'arrière-plan");
257 − if (shot) await page.screenshot({ path: shot.replace(".png", "-menu.png") });
286 + if (shot && after.opened) await page.screenshot({ path: shot.replace(".png", "-menu.png") });
258 287 await page.keyboard.press("Escape").catch(() => {});
259 288 await page.mouse.click(5, Math.round((page.viewportSize()?.height || 800) * 0.75)).catch(() => {});
260 289 }
modified regression-mobile.mjs +10 −2
@@ -33,8 +33,16 @@ for (const w of widths) {
33 33
34 34 const CHECK = `(() => {
35 35 const iw = window.innerWidth;
36 − const out = { overflow: Math.max(document.documentElement.scrollWidth - iw,
37 − document.body ? document.body.scrollWidth - iw : 0), smallInputs: 0, viewport: null };
36 + const raw = Math.max(document.documentElement.scrollWidth - iw,
37 + document.body ? document.body.scrollWidth - iw : 0);
38 + let overflow = 0;
39 + if (raw > 1) { // scrollWidth ment quand overflow-x:clip — tester le défilement réel
40 + const x0 = window.scrollX;
41 + window.scrollBy(raw, 0);
42 + if (window.scrollX - x0 > 1) overflow = raw;
43 + window.scrollTo(x0, 0);
44 + }
45 + const out = { overflow, smallInputs: 0, viewport: null };
38 46 const mv = document.querySelector('meta[name="viewport"]');
39 47 out.viewport = mv ? mv.getAttribute("content") : null;
40 48 for (const el of document.querySelectorAll("input, select, textarea")) {
modified sync.sh +1 −1
@@ -20,7 +20,7 @@ immo-ka|M4M64a|~/apps/immo-ka|frontend/src/ka
20 20 fabri-ka|M4M64a|~/fabri-ka|frontend/src/ka
21 21 auto-ka|M4M64b|~/auto-ka|frontend/src/ka
22 22 food-ka|M4M64b|~/apps/food-ka|frontend/src/ka
23 −trouve-ka|M2M32|~/trouve-ka|frontend/src/ka
23 +trouve-ka|M2M32|~/trouve-ka|apps/web/ka
24 24 "
25 25
26 26 only=("$@")
modified tokens.css +4 −1
@@ -90,7 +90,10 @@
90 90
91 91 /* ---- Socle ---- */
92 92 * { box-sizing: border-box; }
93 −html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
93 +/* overflow-x: clip sur html ET body — sur WebKit iOS, clip posé sur body seul
94 + ne bloque PAS le défilement latéral du viewport (quirk de propagation) ;
95 + clip (≠ hidden) ne casse pas position:sticky. */
96 +html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; overflow-x: clip; }
94 97 body {
95 98 margin: 0;
96 99 background: var(--paper);
97 100