SPB Git

spb/resto-ka

Public

Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)

Mobile : menu compte via createPortal au-dessus de la tabbar, verrou scroll de la feuille filtres, logo tactile

- frontend/src/App.tsx : backdrop + menu compte rendus à la racine de <body>
  via createPortal (le backdrop-filter du header re-parentait le
  position:fixed → voile de 66 px, et le stacking context z-50 du header
  plaquait le menu SOUS la tabbar z-85 et le fab z-80) ; fermeture à Escape.
- frontend/src/styles.css : .account-backdrop → var(--z-overlay) (800),
  .account-menu → position:fixed ancré sous le header (--header-h 66/58 px) à
  var(--z-modal) (900) ; zone tactile ≥44 px du logo .brand (::after inset
  -8px, rendu inchangé) ; les 7 selects de filtres étaient déjà couverts par
  la règle 16 px ≤900 px + filet anti-zoom global de tokens.css.
- frontend/src/pages/Home.tsx : feuille de filtres mobile (sheetOpen) — gel du
  scroll d arrière-plan (pattern App.tsx) + fermeture à Escape.
- frontend/src/ka/tokens.css (commité avec Stats v2) : socle mobile commun.

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

3 changed files +50 −7

modified frontend/src/App.tsx +17 −2
@@ -5,6 +5,7 @@
5 // même patron que le App.tsx de Lou·Ka. 5 // même patron que le App.tsx de Lou·Ka.
6 // ============================================================================== 6 // ==============================================================================
7 import { useEffect, useState } from "react"; 7 import { useEffect, useState } from "react";
8+import { createPortal } from "react-dom";
8 import { NavLink, Route, Routes, useLocation } from "react-router-dom"; 9 import { NavLink, Route, Routes, useLocation } from "react-router-dom";
9 import { AccountProvider, useAccount } from "./account"; 10 import { AccountProvider, useAccount } from "./account";
10 import { 11 import {
@@ -34,6 +35,14 @@ function AccountMenu() {
34 fetchAuthConfig().then((c) => setEnabled(c.ka)).catch(() => {}); 35 fetchAuthConfig().then((c) => setEnabled(c.ka)).catch(() => {});
35 }, []); 36 }, []);
36 37
38+ // menu ouvert : fermeture à Escape (le tap extérieur passe par le backdrop)
39+ useEffect(() => {
40+ if (!menuOpen) return;
41+ const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setMenuOpen(false); };
42+ window.addEventListener("keydown", onKey);
43+ return () => window.removeEventListener("keydown", onKey);
44+ }, [menuOpen]);
45+
37 if (!enabled) return null; 46 if (!enabled) return null;
38 if (!me) { 47 if (!me) {
39 return ( 48 return (
@@ -55,7 +64,12 @@ function AccountMenu() {
55 ? <img src={me.picture} alt="" referrerPolicy="no-referrer" /> 64 ? <img src={me.picture} alt="" referrerPolicy="no-referrer" />
56 : <span className="account-initial">{(me.name || me.email).charAt(0).toUpperCase()}</span>} 65 : <span className="account-initial">{(me.name || me.email).charAt(0).toUpperCase()}</span>}
57 </button> 66 </button>
58− {menuOpen && ( 67+ {/* Backdrop + menu rendus à la racine via createPortal : le
68+ backdrop-filter du header re-parentait le position:fixed (voile de
69+ 66 px) et le stacking context du header (z-50) plaquait le menu SOUS
70+ la tabbar (85) et le fab (80). Le menu devient fixed, ancré sous le
71+ header (voir .account-menu dans styles.css). */}
72+ {menuOpen && createPortal(
59 <> 73 <>
60 <div className="account-backdrop" onClick={() => setMenuOpen(false)} aria-hidden="true" /> 74 <div className="account-backdrop" onClick={() => setMenuOpen(false)} aria-hidden="true" />
61 <div className="account-menu" role="menu"> 75 <div className="account-menu" role="menu">
@@ -79,7 +93,8 @@ function AccountMenu() {
79 Se déconnecter 93 Se déconnecter
80 </button> 94 </button>
81 </div> 95 </div>
82− </> 96+ </>,
97+ document.body,
83 )} 98 )}
84 </div> 99 </div>
85 ); 100 );
modified frontend/src/pages/Home.tsx +13 −0
@@ -68,6 +68,19 @@ export default function Home() {
68 const [statChips, setStatChips] = useState<{ label: string; value: number }[]>([]); 68 const [statChips, setStatChips] = useState<{ label: string; value: number }[]>([]);
69 const [sheetOpen, setSheetOpen] = useState(false); 69 const [sheetOpen, setSheetOpen] = useState(false);
70 70
71+ // feuille de filtres ouverte : gel du scroll d'arrière-plan + fermeture à
72+ // Escape (même pattern que le menu hamburger d'App.tsx)
73+ useEffect(() => {
74+ document.body.style.overflow = sheetOpen ? "hidden" : "";
75+ if (!sheetOpen) return () => { document.body.style.overflow = ""; };
76+ const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setSheetOpen(false); };
77+ window.addEventListener("keydown", onKey);
78+ return () => {
79+ document.body.style.overflow = "";
80+ window.removeEventListener("keydown", onKey);
81+ };
82+ }, [sheetOpen]);
83+
71 const filters = useMemo(() => { 84 const filters = useMemo(() => {
72 const f: Partial<Record<FilterKey, string>> = {}; 85 const f: Partial<Record<FilterKey, string>> = {};
73 for (const k of FILTER_KEYS) { 86 for (const k of FILTER_KEYS) {
modified frontend/src/styles.css +20 −5
@@ -36,7 +36,8 @@ img { display: block; }
36 backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); 36 backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
37 border-bottom: 1.5px solid var(--ink); 37 border-bottom: 1.5px solid var(--ink);
38 } 38 }
39−.header-inner { display: flex; align-items: center; gap: 20px; height: 66px; } 39+:root { --header-h: 66px; } /* hauteur du header (ancre du menu compte fixed) */
40+.header-inner { display: flex; align-items: center; gap: 20px; height: var(--header-h); }
40 .brand { 41 .brand {
41 font-family: var(--font-display); font-weight: 700; font-size: 24px; 42 font-family: var(--font-display); font-weight: 700; font-size: 24px;
42 letter-spacing: -0.04em; display: flex; align-items: center; line-height: 1; 43 letter-spacing: -0.04em; display: flex; align-items: center; line-height: 1;
@@ -583,7 +584,7 @@ img { display: block; }
583 .fab { display: none; } 584 .fab { display: none; }
584 585
585 @media (max-width: 640px) { 586 @media (max-width: 640px) {
586− .header-inner { height: 58px; } 587+ :root { --header-h: 58px; }
587 .brand { font-size: 20px; } 588 .brand { font-size: 20px; }
588 .nav a { padding: 8px 13px; font-size: 13.5px; } 589 .nav a { padding: 8px 13px; font-size: 13.5px; }
589 .ticker { font-size: 10px; padding: 6px 0; } 590 .ticker { font-size: 10px; padding: 6px 0; }
@@ -671,11 +672,18 @@ img { display: block; }
671 .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); } 672 .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }
672 } 673 }
673 674
674−/* Anti-zoom iOS : les champs doivent faire >= 16px */ 675+/* Anti-zoom iOS : les champs doivent faire >= 16px
676+ (filet global : tokens.css force aussi 16 px au doigt) */
675 @media (max-width: 900px) { 677 @media (max-width: 900px) {
676 .f-search input, .f-ctl select { font-size: 16px; } 678 .f-search input, .f-ctl select { font-size: 16px; }
677 } 679 }
678 680
681+/* Cible tactile ≥44 px du logo (138×30 rendus) : zone étendue invisible */
682+@media (pointer: coarse) {
683+ .brand { position: relative; }
684+ .brand::after { content: ""; position: absolute; inset: -8px; }
685+}
686+
679 /* ================= Footer (KaFooter — ka-ui) ================= */ 687 /* ================= Footer (KaFooter — ka-ui) ================= */
680 /* Le footer commun vient de tokens.css (.ka-footer) + KaFooter.tsx. On garde 688 /* Le footer commun vient de tokens.css (.ka-footer) + KaFooter.tsx. On garde
681 ici seulement la rangée de nav locale posée au-dessus. */ 689 ici seulement la rangée de nav locale posée au-dessus. */
@@ -728,9 +736,16 @@ img { display: block; }
728 width: 100%; height: 100%; 736 width: 100%; height: 100%;
729 font-family: var(--font-display); font-weight: 700; font-size: 17px; color: var(--ink); 737 font-family: var(--font-display); font-weight: 700; font-size: 17px; color: var(--ink);
730 } 738 }
731−.account-backdrop { position: fixed; inset: 0; z-index: 55; } 739+/* Backdrop + menu rendus à la racine de <body> via createPortal (un fixed
740+ descendant du header blurré se re-parentait, et le stacking context z-50 du
741+ header plaquait le menu sous la tabbar z-85 / le fab z-80). Échelle commune
742+ tokens.css : voile = --z-overlay (800), menu = --z-modal (900). */
743+.account-backdrop { position: fixed; inset: 0; z-index: var(--z-overlay); }
732 .account-menu { 744 .account-menu {
733− position: absolute; right: 0; top: calc(100% + 10px); z-index: 60; 745+ position: fixed;
746+ right: max(12px, env(safe-area-inset-right));
747+ top: calc(var(--header-h, 66px) + 10px);
748+ z-index: var(--z-modal);
734 min-width: 230px; background: var(--surface); 749 min-width: 230px; background: var(--surface);
735 border: 1.5px solid var(--ink); border-radius: var(--r-card); 750 border: 1.5px solid var(--ink); border-radius: var(--r-card);
736 box-shadow: var(--shadow-off); padding: 8px; display: flex; flex-direction: column; 751 box-shadow: var(--shadow-off); padding: 8px; display: flex; flex-direction: column;
737 752
Menu

Theme